diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..dabcc8d3a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: http://EditorConfig.org +# File take from the VSCode repo at: +# https://github.com/Microsoft/vscode/blob/master/.editorconfig + +# top-most EditorConfig file +root = true + +# Tab indentation +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.xml] +indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..ad136dfa8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,15 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# More details are here: https://help.github.com/articles/about-codeowners/ + +# The '*' pattern is global owners. + +# Order is important. The last matching pattern has the most precedence. +# The folders are ordered as follows: + +# In each subsection folders are ordered first by depth, then alphabetically. +# This should make it easy to add new rules without breaking existing ones. + +# Global rule: +* @microsoft/botframework-sdk \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/java-sdk-bug-report.md b/.github/ISSUE_TEMPLATE/java-sdk-bug-report.md index a1c32e556..f34d7c975 100644 --- a/.github/ISSUE_TEMPLATE/java-sdk-bug-report.md +++ b/.github/ISSUE_TEMPLATE/java-sdk-bug-report.md @@ -1,9 +1,13 @@ --- name: Java SDK Bug Report about: Create a bug report for a bug you found in the Bot Builder Java SDK - +title: "" +labels: "needs-triage, bug" +assignees: "" --- +### [Github issues](https://github.com/microsoft/botbuilder-java/issues) should be used for bugs and feature requests. Use [Stack Overflow](https://stackoverflow.com/questions/tagged/botframework) for general "how-to" questions. + ## Version What package version of the SDK are you using. @@ -25,5 +29,3 @@ If applicable, add screenshots to help explain your problem. ## Additional context Add any other context about the problem here. - -[bug] diff --git a/.github/ISSUE_TEMPLATE/java-sdk-feature-request.md b/.github/ISSUE_TEMPLATE/java-sdk-feature-request.md index 90b1f7b7a..b12c217c2 100644 --- a/.github/ISSUE_TEMPLATE/java-sdk-feature-request.md +++ b/.github/ISSUE_TEMPLATE/java-sdk-feature-request.md @@ -1,9 +1,13 @@ --- name: Java SDK Feature Request about: 'Suggest a feature for the Bot Builder Java SDK ' - +title: "" +labels: "needs-triage, feature-request" +assignees: "" --- +### Use this [query](https://github.com/microsoft/botbuilder-java/issues?q=is%3Aissue+is%3Aopen++label%3Afeature-request+) to search for the most popular feature requests. + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] @@ -15,5 +19,3 @@ A clear and concise description of any alternative solutions or features you've **Additional context** Add any other context or screenshots about the feature request here. - -[enhancement] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..e8870cd7e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +Fixes # + +## Description + + +## Specific Changes + + + - + - + - + +## Testing + \ No newline at end of file diff --git a/.github/workflows/create-parity-issue.yml b/.github/workflows/create-parity-issue.yml new file mode 100644 index 000000000..51f47a190 --- /dev/null +++ b/.github/workflows/create-parity-issue.yml @@ -0,0 +1,43 @@ +name: create-parity-issue.yml + +on: + workflow_dispatch: + inputs: + prDescription: + description: PR description + default: 'No description provided' + required: true + prNumber: + description: PR number + required: true + prTitle: + description: PR title + required: true + sourceRepo: + description: repository PR is sourced from + required: true + +jobs: + createIssue: + name: create issue + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: joshgummersall/create-issue@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: | + port: ${{ github.event.inputs.prTitle }} (#${{ github.event.inputs.prNumber }}) + labels: | + ["parity", "needs-triage", "ExemptFromDailyDRIReport"] + body: | + The changes in [${{ github.event.inputs.prTitle }} (#${{ github.event.inputs.prNumber }})](https://github.com/${{ github.event.inputs.sourceRepo }}/pull/${{ github.event.inputs.prNumber }}) may need to be ported to maintain parity with `${{ github.event.inputs.sourceRepo }}`. + +
+ ${{ github.event.inputs.prDescription }} +
+ + Please review and, if necessary, port the changes. diff --git a/.github/workflows/pr-style.yml b/.github/workflows/pr-style.yml new file mode 100644 index 000000000..51dbf531a --- /dev/null +++ b/.github/workflows/pr-style.yml @@ -0,0 +1,18 @@ +name: pr-style.yml + +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + prStyle: + name: pr-style + runs-on: ubuntu-latest + + steps: + - uses: joshgummersall/pr-style@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + require_issue: "true" + skip_authors: "dependabot" diff --git a/.gitignore b/.gitignore index 9d6304e11..d80f28b79 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,19 @@ target Thumbs.db # reduced pom files should not be included -dependency-reduced-pom.xml \ No newline at end of file +dependency-reduced-pom.xml +*.factorypath +.vscode/settings.json +pom.xml.versionsBackup +libraries/swagger/generated +/javabotframework_pub.gpg +/javabotframework_sec.gpg +/private.pgp +/privatekey.txt +/public.pgp +/.vs/slnx.sqlite +/.vs/botbuilder-java/v16/.suo +/.vs/ProjectSettings.json +/.vs/botbuilder-java/v16/TestStore/0/000.testlog +/.vs/botbuilder-java/v16/TestStore/0/testlog.manifest +/.vs/VSWorkspaceState.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c5f3f6b9c..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "interactive" -} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..f9ba8cf65 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 000000000..41a2e6153 --- /dev/null +++ b/Contributing.md @@ -0,0 +1,23 @@ +# Instructions for Contributing Code + +## Contributing bug fixes and features + +The Bot Framework team is currently accepting contributions in the form of bug fixes and new +features. Any submission must have an issue tracking it in the issue tracker that has + been approved by the Bot Framework team. Your pull request should include a link to + the bug that you are fixing. If you've submitted a PR for a bug, please post a + comment in the bug to avoid duplication of effort. + +## Legal + +If your contribution is more than 15 lines of code, you will need to complete a Contributor +License Agreement (CLA). Briefly, this agreement testifies that you are granting us permission + to use the submitted change according to the terms of the project's license, and that the work + being submitted is under appropriate copyright. + +Please submit a Contributor License Agreement (CLA) before submitting a pull request. +You may visit https://cla.azure.com to sign digitally. Alternatively, download the +agreement ([Microsoft Contribution License Agreement.docx](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=822190) or + [Microsoft Contribution License Agreement.pdf](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=921298)), sign, scan, + and email it back to . Be sure to include your github user name along with the agreement. Once we have received the + signed CLA, we'll review the request. \ No newline at end of file diff --git a/Generator/generator-botbuilder-java/.editorconfig b/Generator/generator-botbuilder-java/.editorconfig deleted file mode 100644 index beffa3084..000000000 --- a/Generator/generator-botbuilder-java/.editorconfig +++ /dev/null @@ -1,11 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/Generator/generator-botbuilder-java/.eslintignore b/Generator/generator-botbuilder-java/.eslintignore deleted file mode 100644 index 515dfdf4f..000000000 --- a/Generator/generator-botbuilder-java/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage -**/templates diff --git a/Generator/generator-botbuilder-java/.gitattributes b/Generator/generator-botbuilder-java/.gitattributes deleted file mode 100644 index 176a458f9..000000000 --- a/Generator/generator-botbuilder-java/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto diff --git a/Generator/generator-botbuilder-java/.travis.yml b/Generator/generator-botbuilder-java/.travis.yml deleted file mode 100644 index 335ea2d0a..000000000 --- a/Generator/generator-botbuilder-java/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js -node_js: - - v10 - - v8 - - v6 - - v4 -after_script: cat ./coverage/lcov.info | coveralls diff --git a/Generator/generator-botbuilder-java/.yo-rc.json b/Generator/generator-botbuilder-java/.yo-rc.json deleted file mode 100644 index 0904532be..000000000 --- a/Generator/generator-botbuilder-java/.yo-rc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "generator-node": { - "promptValues": { - "authorName": "Microsoft", - "authorEmail": "", - "authorUrl": "" - } - } -} \ No newline at end of file diff --git a/Generator/generator-botbuilder-java/LICENSE b/Generator/generator-botbuilder-java/LICENSE deleted file mode 100644 index 08ea44557..000000000 --- a/Generator/generator-botbuilder-java/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/Generator/generator-botbuilder-java/README.md b/Generator/generator-botbuilder-java/README.md deleted file mode 100644 index 4796cad67..000000000 --- a/Generator/generator-botbuilder-java/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# generator-botbuilder-java [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] -> Template to create conversational bots in Java using Microsoft Bot Framework. - -## Installation - -First, install [Yeoman](http://yeoman.io) and generator-botbuilder-java using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). - -```bash -npm install -g yo -npm install -g generator-botbuilder-java -``` - -Then generate your new project: - -```bash -yo botbuilder-java -``` - -## Getting To Know Yeoman - - * Yeoman has a heart of gold. - * Yeoman is a person with feelings and opinions, but is very easy to work with. - * Yeoman can be too opinionated at times but is easily convinced not to be. - * Feel free to [learn more about Yeoman](http://yeoman.io/). - -## License - -MIT © [Microsoft]() - - -[npm-image]: https://badge.fury.io/js/generator-botbuilder-java.svg -[npm-url]: https://npmjs.org/package/generator-botbuilder-java -[travis-image]: https://travis-ci.org/Microsoft/generator-botbuilder-java.svg?branch=master -[travis-url]: https://travis-ci.org/Microsoft/generator-botbuilder-java -[daviddm-image]: https://david-dm.org/Microsoft/generator-botbuilder-java.svg?theme=shields.io -[daviddm-url]: https://david-dm.org/Microsoft/generator-botbuilder-java -[coveralls-image]: https://coveralls.io/repos/Microsoft/generator-botbuilder-java/badge.svg -[coveralls-url]: https://coveralls.io/r/Microsoft/generator-botbuilder-java diff --git a/Generator/generator-botbuilder-java/__tests__/app.js b/Generator/generator-botbuilder-java/__tests__/app.js deleted file mode 100644 index 72f8b04db..000000000 --- a/Generator/generator-botbuilder-java/__tests__/app.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -const path = require('path'); -const assert = require('yeoman-assert'); -const helpers = require('yeoman-test'); - -describe('generator-botbuilder-java:app', () => { - beforeAll(() => { - return helpers - .run(path.join(__dirname, '../generators/app')) - .withPrompts({ someAnswer: true }); - }); - - it('creates files', () => { - assert.file(['dummyfile.txt']); - }); -}); diff --git a/Generator/generator-botbuilder-java/generators/app/index.js b/Generator/generator-botbuilder-java/generators/app/index.js deleted file mode 100644 index 59930463f..000000000 --- a/Generator/generator-botbuilder-java/generators/app/index.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; -const Generator = require('yeoman-generator'); -const chalk = require('chalk'); -const yosay = require('yosay'); -const path = require('path'); -const _ = require('lodash'); -const extend = require('deep-extend'); -const mkdirp = require('mkdirp'); - -module.exports = class extends Generator { - prompting() { - // Have Yeoman greet the user. - this.log( - yosay(`Welcome to the badass ${chalk.red('generator-botbuilder-java')} generator!`) - ); - - const prompts = [ - { name: 'botName', message: `What 's the name of your bot?`, default: 'sample' }, - { name: 'description', message: 'What will your bot do?', default: 'sample' }, - { name: 'dialog', type: 'list', message: 'What default dialog do you want?', choices: ['Echo'] }, - ]; - - return this.prompt(prompts).then(props => { - // To access props later use this.props.someAnswer; - this.props = props; - }); - } - - writing() { - const directoryName = _.kebabCase(this.props.botName); - const defaultDialog = this.props.dialog.split(' ')[0].toLowerCase(); - - if (path.basename(this.destinationPath()) !== directoryName) { - this.log(`Your bot should be in a directory named ${directoryName}\nI'll automatically create this folder.`); - mkdirp(directoryName); - this.destinationRoot(this.destinationPath(directoryName)); - } - - this.fs.copyTpl(this.templatePath('pom.xml'), this.destinationPath('pom.xml'), { botName: directoryName }); - this.fs.copy(this.templatePath(`app.java`), this.destinationPath(`app.java`)); - this.fs.copyTpl(this.templatePath('README.md'), this.destinationPath('README.md'), { - botName: this.props.botName, description: this.props.description - }); - } - - install() { - this.installDependencies({bower: false}); - } -}; diff --git a/Generator/generator-botbuilder-java/generators/app/templates/README.md b/Generator/generator-botbuilder-java/generators/app/templates/README.md deleted file mode 100644 index 47ace33f4..000000000 --- a/Generator/generator-botbuilder-java/generators/app/templates/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# <%= botName %> Bot - -This bot has been created using [Microsoft Bot Framework](https://dev.botframework.com), - -This bot is designed to do the following: - -<%= description %> - -## About the generator - -The goal of the BotBuilder Yeoman generator is to both scaffold out a bot according to general best practices, and to provide some templates you can use when implementing commonly requested features and dialogs in your bot. - -### Dialogs - -This generator provides the following dialogs: -- Echo Dialog, for simple bots - -## Getting Started - -### Dependencies - -### Structure - -### Configuring the bot - -### The dialogs - -- Echo dialog is designed for simple Hello, World demos and to get you started. - -### Running the bot - -## Additional Resources - -- [Microsoft Virtual Academy Bots Course](http://aka.ms/botcourse) -- [Bot Framework Documentation](https://docs.botframework.com) -- [LUIS](https://luis.ai) -- [QnA Maker](https://qnamaker.ai) \ No newline at end of file diff --git a/Generator/generator-botbuilder-java/generators/app/templates/app.java b/Generator/generator-botbuilder-java/generators/app/templates/app.java deleted file mode 100644 index 65584b463..000000000 --- a/Generator/generator-botbuilder-java/generators/app/templates/app.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.sample; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.customizations.CredentialProvider; -import com.microsoft.bot.connector.customizations.CredentialProviderImpl; -import com.microsoft.bot.connector.customizations.JwtTokenValidation; -import com.microsoft.bot.connector.customizations.MicrosoftAppCredentials; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.Activity; -import com.microsoft.bot.schema.models.ActivityTypes; -import com.microsoft.bot.schema.models.ResourceResponse; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.net.URLDecoder; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class App { - private static final Logger LOGGER = Logger.getLogger( App.class.getName() ); - private static String appId = ""; // <-- app id --> - private static String appPassword = ""; // <-- app password --> - - public static void main( String[] args ) throws IOException { - CredentialProvider credentialProvider = new CredentialProviderImpl(appId, appPassword); - HttpServer server = HttpServer.create(new InetSocketAddress(3978), 0); - server.createContext("/api/messages", new MessageHandle(credentialProvider)); - server.setExecutor(null); - server.start(); - } - - static class MessageHandle implements HttpHandler { - private ObjectMapper objectMapper; - private CredentialProvider credentialProvider; - private MicrosoftAppCredentials credentials; - - MessageHandle(CredentialProvider credentialProvider) { - this.objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .findAndRegisterModules(); - this.credentialProvider = credentialProvider; - this.credentials = new MicrosoftAppCredentials(appId, appPassword); - } - - public void handle(HttpExchange httpExchange) throws IOException { - if (httpExchange.getRequestMethod().equalsIgnoreCase("POST")) { - Activity activity = getActivity(httpExchange); - String authHeader = httpExchange.getRequestHeaders().getFirst("Authorization"); - try { - JwtTokenValidation.assertValidActivity(activity, authHeader, credentialProvider); - - // send ack to user activity - httpExchange.sendResponseHeaders(202, 0); - httpExchange.getResponseBody().close(); - - if (activity.type().equals(ActivityTypes.MESSAGE)) { - // reply activity with the same text - ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), this.credentials); - ResourceResponse response = connector.conversations().sendToConversation(activity.conversation().id(), - new Activity() - .withType(ActivityTypes.MESSAGE) - .withText("Echo: " + activity.text()) - .withRecipient(activity.from()) - .withFrom(activity.recipient()) - ); - } - } catch (AuthenticationException ex) { - httpExchange.sendResponseHeaders(401, 0); - httpExchange.getResponseBody().close(); - LOGGER.log(Level.WARNING, "Auth failed!", ex); - } catch (Exception ex) { - LOGGER.log(Level.WARNING, "Execution failed", ex); - } - } - } - - private String getRequestBody(HttpExchange httpExchange) throws IOException { - StringBuilder buffer = new StringBuilder(); - InputStream stream = httpExchange.getRequestBody(); - int rByte; - while ((rByte = stream.read()) != -1) { - buffer.append((char)rByte); - } - stream.close(); - if (buffer.length() > 0) { - return URLDecoder.decode(buffer.toString(), "UTF-8"); - } - return ""; - } - - private Activity getActivity(HttpExchange httpExchange) { - try { - String body = getRequestBody(httpExchange); - LOGGER.log(Level.INFO, body); - return objectMapper.readValue(body, Activity.class); - } catch (Exception ex) { - LOGGER.log(Level.WARNING, "Failed to get activity", ex); - return null; - } - - } - } -} diff --git a/Generator/generator-botbuilder-java/generators/app/templates/pom.xml b/Generator/generator-botbuilder-java/generators/app/templates/pom.xml deleted file mode 100644 index eaad9195d..000000000 --- a/Generator/generator-botbuilder-java/generators/app/templates/pom.xml +++ /dev/null @@ -1,89 +0,0 @@ - - 4.0.0 - - com.microsoft.bot.connector.sample - bot-connector-sample - jar - 1.0.0 - - - com.microsoft.bot - bot-parent - 4.0.0-a0 - ../../ - - - bot-connector-sample - http://maven.apache.org - - - UTF-8 - - - - - junit - junit - 4.12 - test - - - com.fasterxml.jackson.module - jackson-module-parameter-names - 2.9.2 - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - 2.9.2 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.2 - - - org.slf4j - slf4j-api - LATEST - - - org.slf4j - slf4j-simple - LATEST - - - com.microsoft.bot.schema - botbuilder-schema - 4.0.0-a0 - - - com.microsoft.bot.connector - bot-connector - 4.0.0-a0 - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.7.0 - - 1.8 - 1.8 - - - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - com.microsoft.bot.connector.sample.App - - - - - \ No newline at end of file diff --git a/Generator/generator-botbuilder-java/package-lock.json b/Generator/generator-botbuilder-java/package-lock.json deleted file mode 100644 index a68a5382f..000000000 --- a/Generator/generator-botbuilder-java/package-lock.json +++ /dev/null @@ -1,9106 +0,0 @@ -{ - "name": "generator-botbuilder-java", - "version": "0.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz", - "integrity": "sha512-eVXQSbu/RimU6OKcK2/gDJVTFcxXJI4sHbIqw2mhwMZeQ2as/8AhS9DGkEDoHMBBNJZ5B0US63lF56x+KDcxiA==", - "dev": true, - "requires": { - "@babel/highlight": "7.0.0-beta.40" - } - }, - "@babel/highlight": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.40.tgz", - "integrity": "sha512-mOhhTrzieV6VO7odgzFGFapiwRK0ei8RZRhfzHhb6cpX3QM8XXuCLXWjN8qBB7JReDdUR80V3LFfFrGUYevhNg==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^3.0.0" - } - }, - "abab": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", - "dev": true - }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", - "dev": true - }, - "acorn-globals": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", - "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", - "dev": true, - "requires": { - "acorn": "^5.0.0" - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "any-observable": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.2.0.tgz", - "integrity": "sha1-xnhwBYADV5AJCD9UrAq6+1wz0kI=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "app-root-path": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", - "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", - "dev": true - }, - "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", - "dev": true, - "requires": { - "default-require-extensions": "^1.0.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "requires": { - "lodash": "^4.14.0" - } - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", - "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-core": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", - "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.0", - "debug": "^2.6.8", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.7", - "slash": "^1.0.0", - "source-map": "^0.5.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-jest": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-22.4.3.tgz", - "integrity": "sha512-BgSjmtl3mW3i+VeVHEr9d2zFSAT66G++pJcHQiUjd00pkW+voYXFctIm/indcqOWWXw5a1nUpR1XWszD9fJ1qg==", - "dev": true, - "requires": { - "babel-plugin-istanbul": "^4.1.5", - "babel-preset-jest": "^22.4.3" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-istanbul": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", - "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.13.0", - "find-up": "^2.1.0", - "istanbul-lib-instrument": "^1.10.1", - "test-exclude": "^4.2.1" - } - }, - "babel-plugin-jest-hoist": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.3.tgz", - "integrity": "sha512-zhvv4f6OTWy2bYevcJftwGCWXMFe7pqoz41IhMi4xna7xNsX5NygdagsrE0y6kkfuXq8UalwvPwKTyAxME2E/g==", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-preset-jest": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-22.4.3.tgz", - "integrity": "sha512-a+M3LTEXTq3gxv0uBN9Qm6ahUl7a8pj923nFbCUdqFUSsf3YrX8Uc+C3MEwji5Af3LiQjSC7w4ooYewlz8HRTA==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^22.4.3", - "babel-plugin-syntax-object-rest-spread": "^6.13.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" - }, - "dependencies": { - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - } - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binaryextensions": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.1.1.tgz", - "integrity": "sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA==" - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true, - "requires": { - "hoek": "4.x.x" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "browser-process-hrtime": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", - "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", - "dev": true - }, - "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chalk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", - "integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, - "ci-info": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", - "dev": true - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/class-extend/-/class-extend-0.1.2.tgz", - "integrity": "sha1-gFeoKwD1P4Kl1ixQ74z/3sb6vDQ=", - "dev": true, - "requires": { - "object-assign": "^2.0.0" - }, - "dependencies": { - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", - "dev": true - } - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-spinners": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", - "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=", - "dev": true - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "requires": { - "colors": "1.0.3" - } - }, - "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", - "dev": true, - "requires": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=" - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" - }, - "cloneable-readable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "^1.1.1" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "compare-versions": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.1.0.tgz", - "integrity": "sha512-4hAxDSBypT/yp2ySFD346So6Ragw5xmBn/e/agIGl3bZr6DLUqnoRZPusxKrXdYRZpgexO9daejmIenlq/wrIQ==", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", - "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cosmiconfig": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", - "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", - "dev": true, - "requires": { - "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "parse-json": "^4.0.0", - "require-from-string": "^2.0.1" - } - }, - "coveralls": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.0.tgz", - "integrity": "sha512-ZppXR9y5PraUOrf/DzHJY6gzNUhXYE3b9D43xEXs4QYZ7/Oe0Gy0CS+IPKWFfvQFXB3RG9QduaQUFehzSpGAFw==", - "dev": true, - "requires": { - "js-yaml": "^3.6.1", - "lcov-parse": "^0.0.10", - "log-driver": "^1.2.5", - "minimist": "^1.2.0", - "request": "^2.79.0" - } - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "dev": true, - "requires": { - "boom": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.x.x" - } - } - } - }, - "cssom": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", - "dev": true - }, - "cssstyle": { - "version": "0.2.37", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", - "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "dargs": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-5.1.0.tgz", - "integrity": "sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz", - "integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==", - "dev": true, - "requires": { - "abab": "^1.0.4", - "whatwg-mimetype": "^2.0.0", - "whatwg-url": "^6.4.0" - } - }, - "date-fns": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", - "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", - "dev": true - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", - "dev": true, - "requires": { - "strip-bom": "^2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, - "requires": { - "foreach": "^2.0.5", - "object-keys": "^1.0.8" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - }, - "dependencies": { - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-conflict": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/detect-conflict/-/detect-conflict-1.0.1.tgz", - "integrity": "sha1-CIZXpmqWHAUBnbfEIwiDsca0F24=" - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "editions": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", - "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==" - }, - "ejs": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz", - "integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==" - }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true - }, - "error": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", - "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", - "requires": { - "string-template": "~0.2.1", - "xtend": "~4.0.0" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.11.0.tgz", - "integrity": "sha512-ZnQrE/lXTTQ39ulXZ+J1DTFazV9qBy61x2bY071B+qGco8Z8q1QddsLdt/EF8Ai9hcWH72dWS0kFqXLxOxqslA==", - "dev": true, - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, - "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", - "dev": true, - "requires": { - "ajv": "^5.3.0", - "babel-code-frame": "^6.22.0", - "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^3.7.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.4", - "esquery": "^1.0.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.0.1", - "ignore": "^3.3.3", - "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", - "progress": "^2.0.0", - "regexpp": "^1.0.1", - "require-uncached": "^1.0.3", - "semver": "^5.3.0", - "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "4.0.2", - "text-table": "~0.2.0" - } - }, - "eslint-config-prettier": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz", - "integrity": "sha512-ag8YEyBXsm3nmOv1Hz991VtNNDMRa+MNy8cY47Pl4bw6iuzqKbJajXdqUpiw13STdLLrznxgm1hj9NhxeOYq0A==", - "dev": true, - "requires": { - "get-stdin": "^5.0.1" - }, - "dependencies": { - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", - "dev": true - } - } - }, - "eslint-config-xo": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/eslint-config-xo/-/eslint-config-xo-0.20.1.tgz", - "integrity": "sha512-bhDRezvlbYNZn8SHv0WE8aPsdPtH3sq1IU2SznyOtmRwi6e/XQkzs+Kaw1hA9Pz4xmkG796egIsFY2RD6fwUeQ==", - "dev": true - }, - "eslint-plugin-prettier": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz", - "integrity": "sha512-floiaI4F7hRkTrFe8V2ItOK97QYrX75DjmdzmVITZoAP6Cn06oEDPQRsO6MlHEP/u2SxI3xQ52Kpjw6j5WGfeQ==", - "dev": true, - "requires": { - "fast-diff": "^1.1.1", - "jest-docblock": "^21.0.0" - } - }, - "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", - "dev": true - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exec-sh": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", - "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", - "dev": true, - "requires": { - "merge": "^1.1.3" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } - }, - "expect": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-22.4.3.tgz", - "integrity": "sha512-XcNXEPehqn8b/jm8FYotdX0YrXn36qp4HWlrVT4ktwQas1l1LPxiVWncYnnL2eyMtKAmVIaG0XAp0QlrqJaxaA==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "jest-diff": "^22.4.3", - "jest-get-type": "^22.4.3", - "jest-matcher-utils": "^22.4.3", - "jest-message-util": "^22.4.3", - "jest-regex-util": "^22.4.3" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-diff": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", - "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "^2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^1.1.3", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "find-parent-dir": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", - "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", - "dev": true - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "first-chunk-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", - "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "del": "^2.0.2", - "graceful-fs": "^4.1.2", - "write": "^0.2.1" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.x" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.3.0", - "node-pre-gyp": "^0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^0.4.1", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.x.x" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "^1.0.0", - "inherits": "2", - "minimatch": "^3.0.0" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "^4.9.1", - "har-schema": "^1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "~1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "hawk": "3.1.3", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "request": "2.81.0", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^2.2.1", - "tar-pack": "^3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "~0.4.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "~1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~1.0.0", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~4.2.1", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "performance-now": "^0.2.0", - "qs": "~6.4.0", - "safe-buffer": "^5.0.1", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.0.0" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jodid25519": "^1.0.0", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.2.0", - "fstream": "^1.0.10", - "fstream-ignore": "^1.0.5", - "once": "^1.3.3", - "readable-stream": "^2.1.4", - "rimraf": "^2.5.1", - "tar": "^2.2.1", - "uid-number": "^0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", - "dev": true - }, - "get-own-enumerable-property-symbols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz", - "integrity": "sha512-TtY/sbOemiMKPRUDDanGCSgBYe7Mf0vbRsWnBZ+9yghpZ1MvcpSpuZFjHdEeY/LZjZy0vdLjS77L6HosisFiug==", - "dev": true - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "gh-got": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-6.0.0.tgz", - "integrity": "sha512-F/mS+fsWQMo1zfgG9MD8KWvTWPPzzhuVwY++fhQ5Ggd+0P+CAMHtzMZhNxG+TqGfHDChJKsbh6otfMGqO2AKBw==", - "requires": { - "got": "^7.0.0", - "is-plain-obj": "^1.1.0" - } - }, - "github-username": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/github-username/-/github-username-4.1.0.tgz", - "integrity": "sha1-y+KABBiDIG2kISrp5LXxacML9Bc=", - "requires": { - "gh-got": "^6.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "globals": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.4.0.tgz", - "integrity": "sha512-Dyzmifil8n/TmSqYDEXbm+C8yitzJQqQIlJQLNRMwa+BOUJpRC19pyVeN12JAjt61xonvXjtff+hJruTRXn5HA==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "got": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", - "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", - "requires": { - "decompress-response": "^3.2.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-plain-obj": "^1.1.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "p-cancelable": "^0.3.0", - "p-timeout": "^1.1.1", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "url-parse-lax": "^1.0.0", - "url-to-options": "^1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "grouped-queue": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/grouped-queue/-/grouped-queue-0.3.3.tgz", - "integrity": "sha1-wWfSpTGcWg4JZO9qJbfC34mWyFw=", - "requires": { - "lodash": "^4.17.2" - } - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "dev": true, - "requires": { - "function-bind": "^1.0.2" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", - "requires": { - "has-symbol-support-x": "^1.4.1" - } - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true, - "requires": { - "boom": "4.x.x", - "cryptiles": "3.x.x", - "hoek": "4.x.x", - "sntp": "2.x.x" - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", - "dev": true - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, - "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==" - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "husky": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", - "integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==", - "dev": true, - "requires": { - "is-ci": "^1.0.10", - "normalize-path": "^1.0.0", - "strip-indent": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", - "dev": true - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "dev": true, - "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "^1.0.0" - } - }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", - "dev": true - }, - "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", - "dev": true, - "requires": { - "ci-info": "^1.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-generator-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", - "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" - }, - "is-observable": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-0.2.0.tgz", - "integrity": "sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI=", - "dev": true, - "requires": { - "symbol-observable": "^0.2.2" - } - }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" - }, - "is-scoped": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-1.0.0.tgz", - "integrity": "sha1-RJypgpnnEwOCViieyytUDcQ3yzA=", - "requires": { - "scoped-regex": "^1.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", - "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", - "dev": true, - "requires": { - "async": "^2.1.4", - "compare-versions": "^3.1.0", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-hook": "^1.2.0", - "istanbul-lib-instrument": "^1.10.1", - "istanbul-lib-report": "^1.1.4", - "istanbul-lib-source-maps": "^1.2.4", - "istanbul-reports": "^1.3.0", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", - "once": "^1.4.0" - }, - "dependencies": { - "istanbul-lib-source-maps": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz", - "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.0", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz", - "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==", - "dev": true, - "requires": { - "append-transform": "^0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", - "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", - "dev": true, - "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.0", - "semver": "^5.3.0" - } - }, - "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^1.2.0", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz", - "integrity": "sha512-fDa0hwU/5sDXwAklXgAoCJCOsFsBplVQ6WBldz5UwaqOzmDhUK4nfuR7/G//G2lERlblUNJB8P6e8cXq3a7MlA==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.1.2", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - } - }, - "istanbul-reports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", - "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", - "dev": true, - "requires": { - "handlebars": "^4.0.3" - } - }, - "istextorbinary": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", - "integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==", - "requires": { - "binaryextensions": "2", - "editions": "^1.3.3", - "textextensions": "2" - } - }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" - } - }, - "jest": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-22.4.3.tgz", - "integrity": "sha512-FFCdU/pXOEASfHxFDOWUysI/+FFoqiXJADEIXgDKuZyqSmBD3tZ4BEGH7+M79v7czj7bbkhwtd2LaEDcJiM/GQ==", - "dev": true, - "requires": { - "import-local": "^1.0.0", - "jest-cli": "^22.4.3" - }, - "dependencies": { - "jest-cli": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-22.4.3.tgz", - "integrity": "sha512-IiHybF0DJNqZPsbjn4Cy4vcqcmImpoFwNFnkehzVw8lTUSl4axZh5DHewu5bdpZF2Y5gUqFKYzH0FH4Qx2k+UA==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "import-local": "^1.0.0", - "is-ci": "^1.0.10", - "istanbul-api": "^1.1.14", - "istanbul-lib-coverage": "^1.1.1", - "istanbul-lib-instrument": "^1.8.0", - "istanbul-lib-source-maps": "^1.2.1", - "jest-changed-files": "^22.4.3", - "jest-config": "^22.4.3", - "jest-environment-jsdom": "^22.4.3", - "jest-get-type": "^22.4.3", - "jest-haste-map": "^22.4.3", - "jest-message-util": "^22.4.3", - "jest-regex-util": "^22.4.3", - "jest-resolve-dependencies": "^22.4.3", - "jest-runner": "^22.4.3", - "jest-runtime": "^22.4.3", - "jest-snapshot": "^22.4.3", - "jest-util": "^22.4.3", - "jest-validate": "^22.4.3", - "jest-worker": "^22.4.3", - "micromatch": "^2.3.11", - "node-notifier": "^5.2.1", - "realpath-native": "^1.0.0", - "rimraf": "^2.5.4", - "slash": "^1.0.0", - "string-length": "^2.0.0", - "strip-ansi": "^4.0.0", - "which": "^1.2.12", - "yargs": "^10.0.3" - } - } - } - }, - "jest-changed-files": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-22.4.3.tgz", - "integrity": "sha512-83Dh0w1aSkUNFhy5d2dvqWxi/y6weDwVVLU6vmK0cV9VpRxPzhTeGimbsbRDSnEoszhF937M4sDLLeS7Cu/Tmw==", - "dev": true, - "requires": { - "throat": "^4.0.0" - } - }, - "jest-config": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-22.4.3.tgz", - "integrity": "sha512-KSg3EOToCgkX+lIvenKY7J8s426h6ahXxaUFJxvGoEk0562Z6inWj1TnKoGycTASwiLD+6kSYFALcjdosq9KIQ==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^22.4.3", - "jest-environment-node": "^22.4.3", - "jest-get-type": "^22.4.3", - "jest-jasmine2": "^22.4.3", - "jest-regex-util": "^22.4.3", - "jest-resolve": "^22.4.3", - "jest-util": "^22.4.3", - "jest-validate": "^22.4.3", - "pretty-format": "^22.4.3" - } - }, - "jest-diff": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.3.tgz", - "integrity": "sha512-/QqGvCDP5oZOF6PebDuLwrB2BMD8ffJv6TAGAdEVuDx1+uEgrHpSFrfrOiMRx2eJ1hgNjlQrOQEHetVwij90KA==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "diff": "^3.2.0", - "jest-get-type": "^22.4.3", - "pretty-format": "^22.4.3" - } - }, - "jest-docblock": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", - "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", - "dev": true - }, - "jest-environment-jsdom": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz", - "integrity": "sha512-FviwfR+VyT3Datf13+ULjIMO5CSeajlayhhYQwpzgunswoaLIPutdbrnfUHEMyJCwvqQFaVtTmn9+Y8WCt6n1w==", - "dev": true, - "requires": { - "jest-mock": "^22.4.3", - "jest-util": "^22.4.3", - "jsdom": "^11.5.1" - } - }, - "jest-environment-node": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-22.4.3.tgz", - "integrity": "sha512-reZl8XF6t/lMEuPWwo9OLfttyC26A5AMgDyEQ6DBgZuyfyeNUzYT8BFo6uxCCP/Av/b7eb9fTi3sIHFPBzmlRA==", - "dev": true, - "requires": { - "jest-mock": "^22.4.3", - "jest-util": "^22.4.3" - } - }, - "jest-get-type": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", - "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", - "dev": true - }, - "jest-haste-map": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-22.4.3.tgz", - "integrity": "sha512-4Q9fjzuPVwnaqGKDpIsCSoTSnG3cteyk2oNVjBX12HHOaF1oxql+uUiqZb5Ndu7g/vTZfdNwwy4WwYogLh29DQ==", - "dev": true, - "requires": { - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.1.11", - "jest-docblock": "^22.4.3", - "jest-serializer": "^22.4.3", - "jest-worker": "^22.4.3", - "micromatch": "^2.3.11", - "sane": "^2.0.0" - }, - "dependencies": { - "jest-docblock": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-22.4.3.tgz", - "integrity": "sha512-uPKBEAw7YrEMcXueMKZXn/rbMxBiSv48fSqy3uEnmgOlQhSX+lthBqHb1fKWNVmFqAp9E/RsSdBfiV31LbzaOg==", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - } - } - }, - "jest-jasmine2": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-22.4.3.tgz", - "integrity": "sha512-yZCPCJUcEY6R5KJB/VReo1AYI2b+5Ky+C+JA1v34jndJsRcLpU4IZX4rFJn7yDTtdNbO/nNqg+3SDIPNH2ecnw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "co": "^4.6.0", - "expect": "^22.4.3", - "graceful-fs": "^4.1.11", - "is-generator-fn": "^1.0.0", - "jest-diff": "^22.4.3", - "jest-matcher-utils": "^22.4.3", - "jest-message-util": "^22.4.3", - "jest-snapshot": "^22.4.3", - "jest-util": "^22.4.3", - "source-map-support": "^0.5.0" - } - }, - "jest-leak-detector": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-22.4.3.tgz", - "integrity": "sha512-NZpR/Ls7+ndO57LuXROdgCGz2RmUdC541tTImL9bdUtU3WadgFGm0yV+Ok4Fuia/1rLAn5KaJ+i76L6e3zGJYQ==", - "dev": true, - "requires": { - "pretty-format": "^22.4.3" - } - }, - "jest-matcher-utils": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", - "integrity": "sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.4.3", - "pretty-format": "^22.4.3" - } - }, - "jest-message-util": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-22.4.3.tgz", - "integrity": "sha512-iAMeKxhB3Se5xkSjU0NndLLCHtP4n+GtCqV0bISKA5dmOXQfEbdEmYiu2qpnWBDCQdEafNDDU6Q+l6oBMd/+BA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0-beta.35", - "chalk": "^2.0.1", - "micromatch": "^2.3.11", - "slash": "^1.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-22.4.3.tgz", - "integrity": "sha512-+4R6mH5M1G4NK16CKg9N1DtCaFmuxhcIqF4lQK/Q1CIotqMs/XBemfpDPeVZBFow6iyUNu6EBT9ugdNOTT5o5Q==", - "dev": true - }, - "jest-regex-util": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-22.4.3.tgz", - "integrity": "sha512-LFg1gWr3QinIjb8j833bq7jtQopiwdAs67OGfkPrvy7uNUbVMfTXXcOKXJaeY5GgjobELkKvKENqq1xrUectWg==", - "dev": true - }, - "jest-resolve": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-22.4.3.tgz", - "integrity": "sha512-u3BkD/MQBmwrOJDzDIaxpyqTxYH+XqAXzVJP51gt29H8jpj3QgKof5GGO2uPGKGeA1yTMlpbMs1gIQ6U4vcRhw==", - "dev": true, - "requires": { - "browser-resolve": "^1.11.2", - "chalk": "^2.0.1" - } - }, - "jest-resolve-dependencies": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-22.4.3.tgz", - "integrity": "sha512-06czCMVToSN8F2U4EvgSB1Bv/56gc7MpCftZ9z9fBgUQM7dzHGCMBsyfVA6dZTx8v0FDcnALf7hupeQxaBCvpA==", - "dev": true, - "requires": { - "jest-regex-util": "^22.4.3" - } - }, - "jest-runner": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-22.4.3.tgz", - "integrity": "sha512-U7PLlQPRlWNbvOHWOrrVay9sqhBJmiKeAdKIkvX4n1G2tsvzLlf77nBD28GL1N6tGv4RmuTfI8R8JrkvCa+IBg==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "jest-config": "^22.4.3", - "jest-docblock": "^22.4.3", - "jest-haste-map": "^22.4.3", - "jest-jasmine2": "^22.4.3", - "jest-leak-detector": "^22.4.3", - "jest-message-util": "^22.4.3", - "jest-runtime": "^22.4.3", - "jest-util": "^22.4.3", - "jest-worker": "^22.4.3", - "throat": "^4.0.0" - }, - "dependencies": { - "jest-docblock": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-22.4.3.tgz", - "integrity": "sha512-uPKBEAw7YrEMcXueMKZXn/rbMxBiSv48fSqy3uEnmgOlQhSX+lthBqHb1fKWNVmFqAp9E/RsSdBfiV31LbzaOg==", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - } - } - }, - "jest-runtime": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-22.4.3.tgz", - "integrity": "sha512-Eat/esQjevhx9BgJEC8udye+FfoJ2qvxAZfOAWshYGS22HydHn5BgsvPdTtt9cp0fSl5LxYOFA1Pja9Iz2Zt8g==", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-jest": "^22.4.3", - "babel-plugin-istanbul": "^4.1.5", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "exit": "^0.1.2", - "graceful-fs": "^4.1.11", - "jest-config": "^22.4.3", - "jest-haste-map": "^22.4.3", - "jest-regex-util": "^22.4.3", - "jest-resolve": "^22.4.3", - "jest-util": "^22.4.3", - "jest-validate": "^22.4.3", - "json-stable-stringify": "^1.0.1", - "micromatch": "^2.3.11", - "realpath-native": "^1.0.0", - "slash": "^1.0.0", - "strip-bom": "3.0.0", - "write-file-atomic": "^2.1.0", - "yargs": "^10.0.3" - } - }, - "jest-serializer": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-22.4.3.tgz", - "integrity": "sha512-uPaUAppx4VUfJ0QDerpNdF43F68eqKWCzzhUlKNDsUPhjOon7ZehR4C809GCqh765FoMRtTVUVnGvIoskkYHiw==", - "dev": true - }, - "jest-snapshot": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-22.4.3.tgz", - "integrity": "sha512-JXA0gVs5YL0HtLDCGa9YxcmmV2LZbwJ+0MfyXBBc5qpgkEYITQFJP7XNhcHFbUvRiniRpRbGVfJrOoYhhGE0RQ==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-diff": "^22.4.3", - "jest-matcher-utils": "^22.4.3", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^22.4.3" - } - }, - "jest-util": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-22.4.3.tgz", - "integrity": "sha512-rfDfG8wyC5pDPNdcnAlZgwKnzHvZDu8Td2NJI/jAGKEGxJPYiE4F0ss/gSAkG4778Y23Hvbz+0GMrDJTeo7RjQ==", - "dev": true, - "requires": { - "callsites": "^2.0.0", - "chalk": "^2.0.1", - "graceful-fs": "^4.1.11", - "is-ci": "^1.0.10", - "jest-message-util": "^22.4.3", - "mkdirp": "^0.5.1", - "source-map": "^0.6.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "jest-validate": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-22.4.3.tgz", - "integrity": "sha512-CfFM18W3GSP/xgmA4UouIx0ljdtfD2mjeBC6c89Gg17E44D4tQhAcTrZmf9djvipwU30kSTnk6CzcxdCCeSXfA==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-config": "^22.4.3", - "jest-get-type": "^22.4.3", - "leven": "^2.1.0", - "pretty-format": "^22.4.3" - } - }, - "jest-worker": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-22.4.3.tgz", - "integrity": "sha512-B1ucW4fI8qVAuZmicFxI1R3kr2fNeYJyvIQ1rKcuLYnenFV5K5aMbxFj6J0i00Ju83S8jP2d7Dz14+AvbIHRYQ==", - "dev": true, - "requires": { - "merge-stream": "^1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "jsdom": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.8.0.tgz", - "integrity": "sha512-fZZSH6P8tVqYIQl0WKpZuQljPu2cW41Uj/c9omtyGwjwZCB8c82UAi7BSQs/F1FgWovmZsoU02z3k28eHp0Cdw==", - "dev": true, - "requires": { - "abab": "^1.0.4", - "acorn": "^5.3.0", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": ">= 0.2.37 < 0.3.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.0", - "escodegen": "^1.9.0", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.2.0", - "nwmatcher": "^1.4.3", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.83.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.3", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.0", - "ws": "^4.0.0", - "xml-name-validator": "^3.0.0" - } - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lint-staged": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-6.1.1.tgz", - "integrity": "sha512-M/7bwLdXbeG7ZNLcasGeLMBDg60/w6obj3KOtINwJyxAxb53XGY0yH5FSZlWklEzuVbTtqtIfAajh6jYIN90AA==", - "dev": true, - "requires": { - "app-root-path": "^2.0.0", - "chalk": "^2.1.0", - "commander": "^2.11.0", - "cosmiconfig": "^4.0.0", - "debug": "^3.1.0", - "dedent": "^0.7.0", - "execa": "^0.8.0", - "find-parent-dir": "^0.3.0", - "is-glob": "^4.0.0", - "jest-validate": "^21.1.0", - "listr": "^0.13.0", - "lodash": "^4.17.4", - "log-symbols": "^2.0.0", - "minimatch": "^3.0.0", - "npm-which": "^3.0.1", - "p-map": "^1.1.1", - "path-is-inside": "^1.0.2", - "pify": "^3.0.0", - "staged-git-files": "1.0.0", - "stringify-object": "^3.2.0" - }, - "dependencies": { - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "jest-get-type": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz", - "integrity": "sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==", - "dev": true - }, - "jest-validate": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-21.2.1.tgz", - "integrity": "sha512-k4HLI1rZQjlU+EC682RlQ6oZvLrE5SCh3brseQc24vbZTxzT/k/3urar5QMCVgjadmSO7lECeGdc6YxnM3yEGg==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^21.2.0", - "leven": "^2.1.0", - "pretty-format": "^21.2.1" - } - }, - "pretty-format": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-21.2.1.tgz", - "integrity": "sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - } - } - }, - "listr": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.13.0.tgz", - "integrity": "sha1-ILsLowuuZg7oTMBQPfS+PVYjiH0=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "figures": "^1.7.0", - "indent-string": "^2.1.0", - "is-observable": "^0.2.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.4.0", - "listr-verbose-renderer": "^0.4.0", - "log-symbols": "^1.0.2", - "log-update": "^1.0.2", - "ora": "^0.2.3", - "p-map": "^1.1.1", - "rxjs": "^5.4.2", - "stream-to-observable": "^0.2.0", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true - }, - "listr-update-renderer": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz", - "integrity": "sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^1.0.2", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-cursor": "^1.0.2", - "date-fns": "^1.27.2", - "figures": "^1.7.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "requires": { - "chalk": "^2.0.1" - } - }, - "log-update": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", - "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", - "dev": true, - "requires": { - "ansi-escapes": "^1.0.0", - "cli-cursor": "^1.0.2" - }, - "dependencies": { - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - } - } - }, - "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "dev": true, - "requires": { - "js-tokens": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", - "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", - "requires": { - "pify": "^3.0.0" - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "mem-fs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-1.1.3.tgz", - "integrity": "sha1-uK6NLj/Lb10/kWXBLUVRoGXZicw=", - "requires": { - "through2": "^2.0.0", - "vinyl": "^1.1.0", - "vinyl-file": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "mem-fs-editor": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-3.0.2.tgz", - "integrity": "sha1-3Qpuryu4prN3QAZ6pUnrUwEFr58=", - "requires": { - "commondir": "^1.0.1", - "deep-extend": "^0.4.0", - "ejs": "^2.3.1", - "glob": "^7.0.3", - "globby": "^6.1.0", - "mkdirp": "^0.5.0", - "multimatch": "^2.0.0", - "rimraf": "^2.2.8", - "through2": "^2.0.0", - "vinyl": "^2.0.1" - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "requires": { - "mime-db": "~1.33.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "mimic-response": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz", - "integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-odd": "^2.0.0", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "semver": "^5.4.1", - "shellwords": "^0.1.1", - "which": "^1.3.0" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", - "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=", - "dev": true - }, - "npm-path": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", - "integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==", - "dev": true, - "requires": { - "which": "^1.2.10" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npm-which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz", - "integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=", - "dev": true, - "requires": { - "commander": "^2.9.0", - "npm-path": "^2.0.2", - "which": "^1.2.10" - } - }, - "nsp": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/nsp/-/nsp-2.8.1.tgz", - "integrity": "sha512-jvjDg2Gsw4coD/iZ5eQddsDlkvnwMCNnpG05BproSnuG+Gr1bSQMwWMcQeYje+qdDl3XznmhblMPLpZLecTORQ==", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "cli-table": "^0.3.1", - "cvss": "^1.0.0", - "https-proxy-agent": "^1.0.0", - "joi": "^6.9.1", - "nodesecurity-npm-utils": "^5.0.0", - "path-is-absolute": "^1.0.0", - "rc": "^1.1.2", - "semver": "^5.0.3", - "subcommand": "^2.0.3", - "wreck": "^6.3.0" - }, - "dependencies": { - "agent-base": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", - "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", - "dev": true, - "requires": { - "extend": "~3.0.0", - "semver": "~5.0.1" - }, - "dependencies": { - "semver": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", - "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", - "dev": true - } - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "dev": true, - "requires": { - "colors": "1.0.3" - } - }, - "cliclopts": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cliclopts/-/cliclopts-1.1.1.tgz", - "integrity": "sha1-aUMcfLWvcjd0sNORG0w3USQxkQ8=", - "dev": true - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "dev": true - }, - "cvss": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cvss/-/cvss-1.0.2.tgz", - "integrity": "sha1-32fpK/EqeW9J6Sh5nI2zunS5/NY=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "https-proxy-agent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", - "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", - "dev": true, - "requires": { - "agent-base": "2", - "debug": "2", - "extend": "3" - } - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", - "dev": true - }, - "isemail": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", - "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=", - "dev": true - }, - "joi": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", - "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", - "dev": true, - "requires": { - "hoek": "2.x.x", - "isemail": "1.x.x", - "moment": "2.x.x", - "topo": "1.x.x" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "moment": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", - "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nodesecurity-npm-utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nodesecurity-npm-utils/-/nodesecurity-npm-utils-5.0.0.tgz", - "integrity": "sha1-Baow3jDKjIRcQEjpT9eOXgi1Xtk=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "rc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", - "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", - "dev": true, - "requires": { - "deep-extend": "~0.4.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "subcommand": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/subcommand/-/subcommand-2.1.0.tgz", - "integrity": "sha1-XkzspaN3njNlsVEeBfhmh3MC92A=", - "dev": true, - "requires": { - "cliclopts": "^1.1.0", - "debug": "^2.1.3", - "minimist": "^1.2.0", - "xtend": "^4.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "topo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", - "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "wreck": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/wreck/-/wreck-6.3.0.tgz", - "integrity": "sha1-oTaXafB7u2LWo3gzanhx/Hc8dAs=", - "dev": true, - "requires": { - "boom": "2.x.x", - "hoek": "2.x.x" - } - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - } - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "nwmatcher": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", - "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "ora": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", - "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "cli-cursor": "^1.0.2", - "cli-spinners": "^0.1.2", - "object-assign": "^4.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true, - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - } - }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "p-cancelable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", - "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-timeout": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", - "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "pad-component": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pad-component/-/pad-component-0.0.1.tgz", - "integrity": "sha1-rR8izhvw/cDW3dkIrxfzUaQEuKw=" - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "prettier": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.12.1.tgz", - "integrity": "sha1-wa0g6APndJ+vkFpAnSNn4Gu+cyU=", - "dev": true - }, - "pretty-bytes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", - "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=" - }, - "pretty-format": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", - "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "read-chunk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz", - "integrity": "sha1-agTAkoAF7Z1C4aasVgDhnLx/9lU=", - "requires": { - "pify": "^3.0.0", - "safe-buffer": "^5.1.1" - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "realpath-native": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.0.tgz", - "integrity": "sha512-XJtlRJ9jf0E1H1SLeJyQ9PGzQD7S65h1pRXEcAeK48doKOnKxcgPeNohJvD5u/2sI9J1oke6E8bZHS/fmW1UiQ==", - "dev": true, - "requires": { - "util.promisify": "^1.0.0" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "requires": { - "resolve": "^1.1.6" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", - "dev": true - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" - }, - "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "hawk": "~6.0.2", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "stringstream": "~0.0.5", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } - }, - "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", - "dev": true, - "requires": { - "lodash": "^4.13.1" - } - }, - "request-promise-native": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", - "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", - "dev": true, - "requires": { - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-from-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", - "integrity": "sha1-xUUjPp19pmFunVmt+zn8n1iGdv8=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "^7.0.5" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", - "dev": true - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=" - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "requires": { - "rx-lite": "*" - } - }, - "rxjs": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.8.tgz", - "integrity": "sha512-Bz7qou7VAIoGiglJZbzbXa4vpX5BmTTN2Dj/se6+SwADtw4SihqBIiEa7VmTXJ8pynvq0iFr5Gx9VLyye1rIxQ==", - "dev": true, - "requires": { - "symbol-observable": "1.0.1" - }, - "dependencies": { - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", - "dev": true - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "dev": true - }, - "sane": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.0.tgz", - "integrity": "sha512-glfKd7YH4UCrh/7dD+UESsr8ylKWRE7UQPoXuz28FgmcF0ViJQhCTCCZHICRKxf8G8O1KdLEn20dcICK54c7ew==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "exec-sh": "^0.2.0", - "fb-watchman": "^2.0.0", - "fsevents": "^1.1.1", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5", - "watch": "~0.18.0" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "scoped-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-1.0.0.tgz", - "integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=" - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shelljs": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.1.tgz", - "integrity": "sha512-YA/iYtZpzFe5HyWVGrb02FjPxc4EMCfpoU/Phg9fQoyMC72u9598OUBrsU8IrtwAKG0tO8IYaqbaLIw+k3IRGA==", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sinon": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", - "integrity": "sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==", - "dev": true, - "requires": { - "diff": "^3.1.0", - "formatio": "1.2.0", - "lolex": "^1.6.0", - "native-promise-only": "^0.8.1", - "path-to-regexp": "^1.7.0", - "samsam": "^1.1.3", - "text-encoding": "0.6.4", - "type-detect": "^4.0.0" - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true, - "requires": { - "hoek": "4.x.x" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", - "dev": true, - "requires": { - "atob": "^2.0.0", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz", - "integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==", - "dev": true, - "requires": { - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", - "dev": true, - "requires": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", - "dev": true - }, - "staged-git-files": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-1.0.0.tgz", - "integrity": "sha1-zbhHg3wfzFLAioctSIPMCHdmioA=", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-to-observable": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.2.0.tgz", - "integrity": "sha1-WdbqOT2HwsDdrBCqDVYbxrpvDhA=", - "dev": true, - "requires": { - "any-observable": "^0.2.0" - } - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-object": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.2.tgz", - "integrity": "sha512-O696NF21oLiDy8PhpWu8AEqoZHw++QW6mUv0UvKZe8gWSdSvMXkiLufK7OmnP27Dro4GU5kb9U7JIO0mBuCRQg==", - "dev": true, - "requires": { - "get-own-enumerable-property-symbols": "^2.0.1", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-bom-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", - "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", - "requires": { - "first-chunk-stream": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-observable": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-0.2.4.tgz", - "integrity": "sha1-lag9smGG1q9+ehjb2XYKL4bQj0A=", - "dev": true - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "dev": true, - "requires": { - "ajv": "^5.2.3", - "ajv-keywords": "^2.1.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", - "slice-ansi": "1.0.0", - "string-width": "^2.1.1" - } - }, - "taketalk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/taketalk/-/taketalk-1.0.0.tgz", - "integrity": "sha1-tNTw3u0gauffd1sSnqLKbeUvJt0=", - "requires": { - "get-stdin": "^4.0.1", - "minimist": "^1.1.0" - } - }, - "test-exclude": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", - "integrity": "sha512-qpqlP/8Zl+sosLxBcVKl9vYy26T9NPalxSzzCP/OY6K7j938ui2oKgo+kRZYfxAeIpLqpbVnsHq1tyV70E4lWQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "micromatch": "^3.1.8", - "object-assign": "^4.1.0", - "read-pkg-up": "^1.0.1", - "require-main-filename": "^1.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "textextensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.2.0.tgz", - "integrity": "sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA==" - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", - "dev": true - } - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "untildify": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz", - "integrity": "sha1-fx8wIFWz/qDz6B3HjrNnZstl4/E=" - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - }, - "url-to-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" - }, - "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vinyl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", - "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - }, - "vinyl-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", - "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.3.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0", - "strip-bom-stream": "^2.0.0", - "vinyl": "^1.1.0" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "dev": true, - "requires": { - "exec-sh": "^0.2.0", - "minimist": "^1.2.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", - "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.19" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true - } - } - }, - "whatwg-mimetype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", - "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==", - "dev": true - }, - "whatwg-url": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.0.tgz", - "integrity": "sha512-Z0CVh/YE217Foyb488eo+iBv+r7eAQ0wSTyApi9n06jhcA3z6Nidg/EGvl0UFkg7kMdKxfBzzr+o9JF+cevgMg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.0", - "webidl-conversions": "^4.0.1" - } - }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.0.0.tgz", - "integrity": "sha512-QYslsH44bH8O7/W2815u5DpnCpXWpEK44FmaHffNwgJI4JMaSZONgPBTOfrxJ29mXKbXak+LsJ2uAkDTYq2ptQ==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", - "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^8.1.0" - }, - "dependencies": { - "cliui": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", - "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - } - } - }, - "yargs-parser": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", - "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - } - } - }, - "yeoman-assert": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yeoman-assert/-/yeoman-assert-3.1.1.tgz", - "integrity": "sha512-bCuLb/j/WzpvrJZCTdJJLFzm7KK8IYQJ3+dF9dYtNs2CUYyezFJDuULiZ2neM4eqjf45GN1KH/MzCTT3i90wUQ==", - "dev": true - }, - "yeoman-environment": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-2.0.6.tgz", - "integrity": "sha512-jzHBTTy8EPI4ImV8dpUMt+Q5zELkSU5xvGpndHcHudQ4tqN6YgIWaCGmRFl+HDchwRUkcgyjQ+n6/w5zlJBCPg==", - "requires": { - "chalk": "^2.1.0", - "debug": "^3.1.0", - "diff": "^3.3.1", - "escape-string-regexp": "^1.0.2", - "globby": "^6.1.0", - "grouped-queue": "^0.3.3", - "inquirer": "^3.3.0", - "is-scoped": "^1.0.0", - "lodash": "^4.17.4", - "log-symbols": "^2.1.0", - "mem-fs": "^1.1.0", - "text-table": "^0.2.0", - "untildify": "^3.0.2" - } - }, - "yeoman-generator": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-2.0.4.tgz", - "integrity": "sha512-Sgvz3MAkOpEIobcpW3rjEl6bOTNnl8SkibP9z7hYKfIGIlw0QDC2k0MAeXvyE2pLqc2M0Duql+6R7/W9GrJojg==", - "requires": { - "async": "^2.6.0", - "chalk": "^2.3.0", - "cli-table": "^0.3.1", - "cross-spawn": "^5.1.0", - "dargs": "^5.1.0", - "dateformat": "^3.0.2", - "debug": "^3.1.0", - "detect-conflict": "^1.0.0", - "error": "^7.0.2", - "find-up": "^2.1.0", - "github-username": "^4.0.0", - "istextorbinary": "^2.1.0", - "lodash": "^4.17.4", - "make-dir": "^1.1.0", - "mem-fs-editor": "^3.0.2", - "minimist": "^1.2.0", - "pretty-bytes": "^4.0.2", - "read-chunk": "^2.1.0", - "read-pkg-up": "^3.0.0", - "rimraf": "^2.6.2", - "run-async": "^2.0.0", - "shelljs": "^0.8.0", - "text-table": "^0.2.0", - "through2": "^2.0.0", - "yeoman-environment": "^2.0.5" - } - }, - "yeoman-test": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/yeoman-test/-/yeoman-test-1.7.0.tgz", - "integrity": "sha512-vJeg2gpWfhbq0HvQ7/yqmsQpYmADBfo9kaW+J6uJASkI7ChLBXNLIBQqaXCA65kWtHXOco+nBm0Km/O9YWk25Q==", - "dev": true, - "requires": { - "inquirer": "^3.0.1", - "lodash": "^4.3.0", - "mkdirp": "^0.5.1", - "pinkie-promise": "^2.0.1", - "rimraf": "^2.4.4", - "sinon": "^2.3.6", - "yeoman-environment": "^2.0.0", - "yeoman-generator": "^1.1.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "diff": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", - "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=", - "dev": true - }, - "external-editor": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", - "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "spawn-sync": "^1.0.15", - "tmp": "^0.0.29" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "gh-got": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-5.0.0.tgz", - "integrity": "sha1-7pW+NxBv2HSKlvjR20uuqJ4b+oo=", - "dev": true, - "requires": { - "got": "^6.2.0", - "is-plain-obj": "^1.1.0" - } - }, - "github-username": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/github-username/-/github-username-3.0.0.tgz", - "integrity": "sha1-CnciGbMTB0NCnyRW0L3T21Xc57E=", - "dev": true, - "requires": { - "gh-got": "^5.0.0" - } - }, - "globby": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz", - "integrity": "sha1-CA9UVJ7BuCpsYOYx/ILhIR2+lfg=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^6.0.1", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - } - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "mute-stream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", - "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=", - "dev": true - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.1" - } - }, - "untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha1-F+soB5h/dpUunASF/DEdBqgmouA=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "yeoman-generator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-1.1.1.tgz", - "integrity": "sha1-QMK09s374F4ZUv3XKTPw2JJdvfU=", - "dev": true, - "requires": { - "async": "^2.0.0", - "chalk": "^1.0.0", - "class-extend": "^0.1.0", - "cli-table": "^0.3.1", - "cross-spawn": "^5.0.1", - "dargs": "^5.1.0", - "dateformat": "^2.0.0", - "debug": "^2.1.0", - "detect-conflict": "^1.0.0", - "error": "^7.0.2", - "find-up": "^2.1.0", - "github-username": "^3.0.0", - "glob": "^7.0.3", - "istextorbinary": "^2.1.0", - "lodash": "^4.11.1", - "mem-fs-editor": "^3.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.0", - "path-exists": "^3.0.0", - "path-is-absolute": "^1.0.0", - "pretty-bytes": "^4.0.2", - "read-chunk": "^2.0.0", - "read-pkg-up": "^2.0.0", - "rimraf": "^2.2.0", - "run-async": "^2.0.0", - "shelljs": "^0.7.0", - "text-table": "^0.2.0", - "through2": "^2.0.0", - "user-home": "^2.0.0", - "yeoman-environment": "^1.1.0" - }, - "dependencies": { - "inquirer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", - "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "external-editor": "^1.1.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "mute-stream": "0.0.6", - "pinkie-promise": "^2.0.0", - "run-async": "^2.2.0", - "rx": "^4.1.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - } - }, - "yeoman-environment": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-1.6.6.tgz", - "integrity": "sha1-zYX6Z9FWBg5EDXgH1+988NLR1nE=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "debug": "^2.0.0", - "diff": "^2.1.2", - "escape-string-regexp": "^1.0.2", - "globby": "^4.0.0", - "grouped-queue": "^0.3.0", - "inquirer": "^1.0.2", - "lodash": "^4.11.1", - "log-symbols": "^1.0.1", - "mem-fs": "^1.1.0", - "text-table": "^0.2.0", - "untildify": "^2.0.0" - } - } - } - } - } - }, - "yosay": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/yosay/-/yosay-2.0.2.tgz", - "integrity": "sha512-avX6nz2esp7IMXGag4gu6OyQBsMh/SEn+ZybGu3yKPlOTE6z9qJrzG/0X5vCq/e0rPFy0CUYCze0G5hL310ibA==", - "requires": { - "ansi-regex": "^2.0.0", - "ansi-styles": "^3.0.0", - "chalk": "^1.0.0", - "cli-boxes": "^1.0.0", - "pad-component": "0.0.1", - "string-width": "^2.0.0", - "strip-ansi": "^3.0.0", - "taketalk": "^1.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - } - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - } - } -} diff --git a/Generator/generator-botbuilder-java/package.json b/Generator/generator-botbuilder-java/package.json deleted file mode 100644 index 82a38d3f9..000000000 --- a/Generator/generator-botbuilder-java/package.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "generator-botbuilder-java", - "version": "0.0.0", - "description": "Template to create conversational bots in Java using Microsoft Bot Framework.", - "homepage": "https://github.com/Microsoft/botbuilder-java", - "author": { - "name": "Microsoft", - "email": "", - "url": "" - }, - "files": [ - "generators" - ], - "main": "generators/index.js", - "keywords": [ - "bot", - "bots", - "chatbots", - "bot framework", - "yeoman-generator" - ], - "devDependencies": { - "yeoman-test": "^1.7.0", - "yeoman-assert": "^3.1.0", - "coveralls": "^3.0.0", - "nsp": "^2.8.0", - "eslint": "^4.19.1", - "prettier": "^1.11.1", - "husky": "^0.14.3", - "lint-staged": "^6.1.1", - "eslint-config-prettier": "^2.9.0", - "eslint-plugin-prettier": "^2.6.0", - "eslint-config-xo": "^0.20.1", - "jest": "^22.0.6" - }, - "engines": { - "npm": ">= 4.0.0" - }, - "dependencies": { - "yeoman-generator": "^2.0.1", - "chalk": "^2.1.0", - "yosay": "^2.0.1" - }, - "jest": { - "testEnvironment": "node" - }, - "scripts": { - "prepublishOnly": "nsp check", - "pretest": "eslint .", - "precommit": "lint-staged", - "test": "jest" - }, - "lint-staged": { - "*.js": [ - "eslint --fix", - "git add" - ], - "*.json": [ - "prettier --write", - "git add" - ] - }, - "eslintConfig": { - "extends": [ - "xo", - "prettier" - ], - "env": { - "jest": true, - "node": true - }, - "rules": { - "prettier/prettier": [ - "error", - { - "singleQuote": true, - "printWidth": 90 - } - ] - }, - "plugins": [ - "prettier" - ] - }, - "repository": "https://github.com/Microsoft/botbuilder-java.git", - "license": "MIT" -} diff --git a/README.md b/README.md index 842e07bac..7c90ffb18 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,129 @@ +# ![Bot Framework for Java](./docs/media/BotFrameworkJava_header.png) -# Bot Builder SDK v4 (Java) (Preview) +**The Bot Framework Java SDK is being retired with final long-term support ending in November 2023, after which this repository will be archived. There will be no further feature development, with only critical security and bug fixes within this repository being undertaken. Existing bots built with this SDK will continue to function. For all new bot development we recommend that you adopt [Power Virtual Agents](https://powervirtualagents.microsoft.com/en-us/blog/the-future-of-bot-building/) or use the [Bot Framework C#](https://github.com/microsoft/botbuilder-dotnet) or [Bot Framework JavaScript](https://github.com/microsoft/botbuilder-js) SDKs.** -[![Build Status](https://travis-ci.org/Microsoft/botbuilder-java.svg?branch=master)](https://travis-ci.org/Microsoft/botbuilder-java) -[![roadmap badge](https://img.shields.io/badge/visit%20the-roadmap-blue.svg)](https://github.com/Microsoft/botbuilder-java/wiki/Roadmap) +This repository contains code for the Java version of the [Microsoft Bot Framework SDK](https://github.com/Microsoft/botframework-sdk), which is part of the Microsoft Bot Framework - a comprehensive framework for building enterprise-grade conversational AI experiences. -This repository contains code for the Java version of the [Microsoft Bot Builder V4 SDK](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0). The Bot Builder SDK v4 is the latest SDK for building bot applications. It is in **Preview** state and is being actively developed. -Production bots should continue to be developed using C# or Javascript with the [v3 SDK](https://github.com/Microsoft/BotBuilder/tree/master/CSharp). In addition to the Java Bot Builder V4 SDK, Bot Builder supports creating bots in other popular programming languages like [.Net SDK](https://github.com/Microsoft/botbuilder-dotnet), [JavaScript](https://github.com/Microsoft/botbuilder-js), and [Python](https://github.com/Microsoft/botbuilder-python). +This SDK enables developers to model conversation and build sophisticated bot applications using Java. SDKs for [.NET](https://github.com/Microsoft/botbuilder-dotnet), [Python](https://github.com/Microsoft/botbuilder-python) and [JavaScript](https://github.com/Microsoft/botbuilder-js) are also available. -To get started see the [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0) for the v4 SDK. +To get started building bots using the SDK, see the [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0). If you are an existing user, then you can also [find out what's new with Bot Framework](https://docs.microsoft.com/en-us/azure/bot-service/what-is-new?view=azure-bot-service-4.0). -## Contributing +For more information jump to a section below. -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. +- [Bot Framework for Java](#) + - [Build Status](#build-status) + - [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Clone](#clone) + - [Build and test locally](#build-and-test-locally) + - [Linting rules](#linting-rules) + - [Getting support and providing feedback](#getting-support-and-providing-feedback) + - [Github issues](#github-issues) + - [Stack overflow](#stack-overflow) + - [Azure Support](#azure-support) + - [Twitter](#twitter) + - [Gitter Chat Room](#gitter-chat-room) + - [Contributing and our code of conduct](#contributing-and-our-code-of-conduct) + - [Reporting Security Issues](#reporting-security-issues) -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +## Build Status + + | Branch | Description | Build Status | Coverage Status | + |--------|-------------|--------------|-----------------| + |Main | 4.15.* Builds | [![Build Status](https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Java/BotBuilder-Java-4.0-daily?branchName=main)](https://fuselabs.visualstudio.com/SDK_v4/_build/latest?definitionId=1202&branchName=main) | [![Coverage Status](https://coveralls.io/repos/github/microsoft/botbuilder-java/badge.svg?branch=823847c676b7dbb0fa348a308297ae375f5141ef)](https://coveralls.io/github/microsoft/botbuilder-java?branch=823847c676b7dbb0fa348a308297ae375f5141ef) | + +## Getting Started +To get started building bots using the SDK, see the [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0). + +The [Bot Framework Samples](https://github.com/microsoft/botbuilder-samples) includes a rich set of samples repository. + +If you want to debug an issue, would like to [contribute](#contributing), or understand how the Bot Builder SDK works, instructions for building and testing the SDK are below. + +### Prerequisites +- [Git](https://git-scm.com/downloads) +- [Java](https://www.azul.com/downloads/zulu/) +- [Maven](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html) + +### Clone +Clone a copy of the repo: +```bash +git clone https://github.com/Microsoft/botbuilder-java.git +``` +Change to the SDK's directory: +```bash +cd botbuilder-java +``` + +Now at the command prompt type: +```bash +mvn clean install +``` + +### Build and test locally +Any IDE that can import and work with Maven projects should work. As a matter of practice we use the command line to perform Maven builds. If your IDE can be configured to defer build and run to Maven it should also work. +- Java + - We use the [Azul JDK 8](https://www.azul.com/downloads/azure-only/zulu/?version=java-8-lts&architecture=x86-64-bit&package=jdk) to build and test with. While not a requirement to develop the SDK with, it is recommended as this is what Azure is using for Java 1.8. If you do install this JDK, make sure your IDE is targeting that JVM, and your path (from command line) and JAVA_HOME point to that. + +- Visual Studio Code + - Extensions + - Java Extension Pack by Microsoft + - EditorConfig for VS Code by EditorConfig (Recommended) + +- IntelliJ + - Extensions + - Checkstyle by IDEA + - Recommended setup + - When importing the SDK for the first time, make sure "Auto import" is checked. + +### Linting rules + +This project uses linting rules to enforce code standardization. These rules are specified in the file [bot-checkstyle.xml](./etc/bot-checkstyle.xml) with [CheckStyle](https://checkstyle.org/) and are hooked to Maven's build cycle. + +**INFO**: Since the CheckStyle and PMD plugins are hooked into the build cycle, this makes the build **fail** in cases where there are linting warnings in the project files. Errors will be in the file ./target/checkstyle-result.xml and ./target/pmd.xml. + +CheckStyle is available in different flavours: +- [Visual Studio Code plugin](https://marketplace.visualstudio.com/items?itemName=shengchen.vscode-checkstyle) +- [IntelliJ IDEA plugin](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) +- [Eclipse plugin](https://checkstyle.org/eclipse-cs) +- [CLI Tool](https://checkstyle.org/cmdline.html) + +**INFO**: Be sure to configure your IDE to use the file [bot-checkstyle.xml](./etc/bot-checkstyle.xml) instead of the default rules. + +## Getting support and providing feedback +Below are the various channels that are available to you for obtaining support and providing feedback. Please pay carful attention to which channel should be used for which type of content. e.g. general "how do I..." questions should be asked on Stack Overflow, Twitter or Gitter, with GitHub issues being for feature requests and bug reports. + +### Github issues +[Github issues](https://github.com/Microsoft/botbuilder-python/issues) should be used for bugs and feature requests. + +### Stack overflow +[Stack Overflow](https://stackoverflow.com/questions/tagged/botframework) is a great place for getting high-quality answers. Our support team, as well as many of our community members are already on Stack Overflow providing answers to 'how-to' questions. + +### Azure Support +If you issues relates to [Azure Bot Service](https://azure.microsoft.com/en-gb/services/bot-service/), you can take advantage of the available [Azure support options](https://azure.microsoft.com/en-us/support/options/). + +### Twitter +We use the [@botframework](https://twitter.com/botframework) account on twitter for announcements and members from the development team watch for tweets for @botframework. + +### Gitter Chat Room +The [Gitter Channel](https://gitter.im/Microsoft/BotBuilder) provides a place where the Community can get together and collaborate. + +## Contributing and our code of conduct +We welcome contributions and suggestions. Please see our [contributing guidelines](./Contributing.md) for more information. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact + [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Reporting Security Issues +Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) +at [secure@microsoft.com](mailto:secure@microsoft.com). You should receive a response within 24 hours. If for some + reason you do not, please follow up via email to ensure we received your original message. Further information, + including the [MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in the +[Security TechCenter](https://technet.microsoft.com/en-us/security/default). -Security issues and bugs should be reported privately, via email, to the Microsoft Security -Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). You should -receive a response within 24 hours. If for some reason you do not, please follow up via -email to ensure we received your original message. Further information, including the -[MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in -the [Security TechCenter](https://technet.microsoft.com/en-us/security/default). +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [MIT](./LICENSE.md) License. -## License -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the [MIT](https://github.com/Microsoft/vscode/blob/master/LICENSE.txt) License. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e138ec5d6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 000000000..88e0b4c9a --- /dev/null +++ b/STATUS.md @@ -0,0 +1,70 @@ +# Java Bot Framework + +The current release is **Preview 8**. + +## Core Packages + +| Package | Status +| :------------- |:------------- +| bot-schema | Preview 2 +| bot-connector | Preview 2 +| bot-integration-core | Preview 2 +| bot-builder | Preview 3 +| bot-builder-teams | Preview 5 +| bot-schema-teams | Preview 5 +| bot-connector-teams | Preview 5 +| bot-dialog | Preview 8 +| bot-dialog-adaptive | TBD +| bot-dialog-declarative | TBD +| bot-streaming-extensions | TBD +| bot-ai-luis-v3 | Preview 8 +| bot-ai-qna | In Progress +| bot-applicationinsights | Not Started +| bot-azure | In Progress + +## Samples +| Package | Status +| ------------- |:------------- +| 02.echo-bot | Preview 3 +| 03.welcome-user | Preview 3 +| 05.multi-turn-prompt | Preview 8 +| 06.using-cards | Preview 8 +| 07.using-adaptive-cards | +| 08.suggested-actions | Preview 3 +| 11.qnamaker | +| 13.core-bot | +| 14.nlp-with-dispatch | +| 15.handling-attachments | +| 16.proactive-messages | Preview 3 +| 17.multilingual-bot | Preview 8 +| 18.bot-authentication | Preview 8 +| 19.custom-dialogs | Preview 8 +| 21.corebot-app-insights | +| 23.facebook-events | +| 24.bot-authentication-msgraph | Preview 8 +| 40.timex-resolution | +| 42.scaleout | +| 43.complex-dialog | Preview 8 +| 44.prompt-users-for-input | Preview 8 +| 45.state-management | Preview 3 +| 46.teams-auth | Preview 8 +| 47.inspection | Preview 3 +| 48.qnamaker-active-learning-bot | +| 49.qnamaker-all-features | +| 50.teams-messaging-extensions-search | Preview 5 +| 51.teams-messaging-extensions-action | Preview 5 +| 52.teams-messaging-extensions-search-auth-config | Preview 5 +| 53.teams-messaging-extensions-action-preview | Preview 5 +| 54.teams-task-module | Preview 5 +| 55.teams-link-unfurling | Preview 5 +| 56.teams-file-upload | Preview 5 +| 57.teams-conversation-bot | Preview 5 +| 58.teams-start-new-thread-in-channel | Preview 5 +| servlet-echo | Preview 2 + +## Build Prerequisites + +- [Java 1.8](https://docs.microsoft.com/en-us/azure/java/jdk/java-jdk-install) + - Should be able to execute `java -version` from command line. +- [Maven](https://maven.apache.org/install.html) + - Should be able to execute `mvn -version` from command line. diff --git a/docs/media/BotFrameworkJava_header.png b/docs/media/BotFrameworkJava_header.png new file mode 100644 index 000000000..4feb13007 Binary files /dev/null and b/docs/media/BotFrameworkJava_header.png differ diff --git a/etc/bot-checkstyle.xml b/etc/bot-checkstyle.xml new file mode 100644 index 000000000..a379f3c79 --- /dev/null +++ b/etc/bot-checkstyle.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/botframework-java-formatter.xml b/etc/botframework-java-formatter.xml new file mode 100644 index 000000000..349eea9a8 --- /dev/null +++ b/etc/botframework-java-formatter.xml @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Generator/generator-botbuilder-java/.gitignore b/generators/.gitignore similarity index 100% rename from Generator/generator-botbuilder-java/.gitignore rename to generators/.gitignore diff --git a/generators/LICENSE.md b/generators/LICENSE.md new file mode 100644 index 000000000..506ab97e5 --- /dev/null +++ b/generators/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/generators/README.md b/generators/README.md new file mode 100644 index 000000000..c86d8685f --- /dev/null +++ b/generators/README.md @@ -0,0 +1,126 @@ +# generator-botbuilder-java + +Yeoman generator for [Bot Framework v4](https://dev.botframework.com). Will let you quickly set up a conversational AI bot +using core AI capabilities. + +## About + +`generator-botbuilder-java` will help you build new conversational AI bots using the [Bot Framework v4](https://dev.botframework.com). + +## Templates + +The generator supports three different template options. The table below can help guide which template is right for you. + +| Template | Description | +| ---------- | --------- | +| Echo Bot | A good template if you want a little more than "Hello World!", but not much more. This template handles the very basics of sending messages to a bot, and having the bot process the messages by repeating them back to the user. This template produces a bot that simply "echoes" back to the user anything the user says to the bot. | +| Empty Bot | A good template if you are familiar with Bot Framework v4, and simply want a basic skeleton project. Also a good option if you want to take sample code from the documentation and paste it into a minimal bot in order to learn. | +| Core Bot | A good template if you want to create advanced bots, as it uses multi-turn dialogs and [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. This template creates a bot that can extract places and dates to book a flight. | + +### How to Choose a Template + +| Template | When This Template is a Good Choice | +| -------- | -------- | +| Echo Bot | You are new to Bot Framework v4 and want a working bot with minimal features. | +| Empty Bot | You are a seasoned Bot Framework v4 developer. You've built bots before, and want the minimum skeleton of a bot. | +| Core Bot | You are a medium to advanced user of Bot Framework v4 and want to start integrating language understanding as well as multi-turn dialogs in your bots. | + +### Template Overview + +#### Echo Bot Template + +The Echo Bot template is slightly more than the a classic "Hello World!" example, but not by much. This template shows the basic structure of a bot, how a bot receives messages from a user, and how a bot sends messages to a user. The bot will "echo" back to the user, what the user says to the bot. It is a good choice for first time, new to Bot Framework v4 developers. + +#### Empty Bot Template + +The Empty Bot template is the minimal skeleton code for a bot. It provides a stub `onTurn` handler but does not perform any actions. If you are experienced writing bots with Bot Framework v4 and want the minimum scaffolding, the Empty template is for you. + +#### Core Bot Template + +The Core Bot template uses [LUIS](https://www.luis.ai) to implement core AI capabilities, a multi-turn conversation using Dialogs, handles user interruptions, and prompts for and validate requests for information from the user. This template implements a basic three-step waterfall dialog, where the first step asks the user for an input to book a flight, then asks the user if the information is correct, and finally confirms the booking with the user. Choose this template if want to create an advanced bot that can extract information from the user's input. + +## Installation + +1. Install [Yeoman](http://yeoman.io) using [npm](https://www.npmjs.com) (we assume you have pre-installed [node.js](https://nodejs.org/)). + + ```bash + # Make sure both are installed globally + npm install -g yo + ``` + +2. Install generator-botbuilder-java by typing the following in your console: + + ```bash + # Make sure both are installed globally + npm install -g generator-botbuilder-java + ``` + +3. Verify that Yeoman and generator-botbuilder-java have been installed correctly by typing the following into your console: + + ```bash + yo botbuilder-java --help + ``` + +## Usage + +### Creating a New Bot Project + +When the generator is launched, it will prompt for the information required to create a new bot. + +```bash +# Run the generator in interactive mode +yo botbuilder-java +``` + +### Generator Command Line Options + +The generator supports a number of command line options that can be used to change the generator's default options or to pre-seed a prompt. + +| Command line Option | Description | +| ------------------- | ----------- | +| --help, -h | List help text for all supported command-line options | +| --botName, -N | The name given to the bot project | +| --packageName, -P | The Java package name to use for the bot | +| --template, -T | The template used to generate the project. Options are `empty`, or `echo`. See [https://aka.ms/botbuilder-generator](https://aka.ms/botbuilder-generator) for additional information regarding the different template option and their functional differences. | +| --noprompt | The generator will not prompt for confirmation before creating a new bot. Any requirement options not passed on the command line will use a reasonable default value. This option is intended to enable automated bot generation for testing purposes. | + +#### Example Using Command Line Options + +This example shows how to pass command line options to the generator, setting the default language to TypeScript and the default template to Core. + +```bash +# Run the generator defaulting the pacakge name and the template +yo botbuilder-java --P "com.mycompany.bot" --T "echo" +``` + +### Generating a Bot Using --noprompt + +The generator can be run in `--noprompt` mode, which can be used for automated bot creation. When run in `--noprompt` mode, the generator can be configured using command line options as documented above. If a command line option is ommitted a reasonable default will be used. In addition, passing the `--noprompt` option will cause the generator to create a new bot project without prompting for confirmation before generating the bot. + +#### Default Options + +| Command line Option | Default Value | +| ------------------- | ----------- | +| --botname, -N | `echo` | +| --packageName, -p | `echo` | +| --template, -T | `echo` | + +#### Examples Using --noprompt + +This example shows how to run the generator in --noprompt mode, setting all required options on the command line. + +```bash +# Run the generator, setting all command line options +yo botbuilder-java --noprompt -N "MyEchoBot" -P "com.mycompany.bot.echo" -T "echo" +``` + +This example shows how to run the generator in --noprompt mode, using all the default command line options. The generator will create a bot project using all the default values specified in the **Default Options** table above. + +```bash +# Run the generator using all default options +yo botbuilder-java --noprompt +``` + +## Logging Issues and Providing Feedback + +Issues and feedback about the botbuilder generator can be submitted through the project's [GitHub Issues](https://github.com/Microsoft/botbuilder-java/issues) page. diff --git a/generators/generators/app/index.js b/generators/generators/app/index.js new file mode 100644 index 000000000..13df547aa --- /dev/null +++ b/generators/generators/app/index.js @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +const pkg = require('../../package.json'); +const Generator = require('yeoman-generator'); +const path = require('path'); +const chalk = require('chalk'); +const mkdirp = require('mkdirp'); +const _ = require('lodash'); + +const BOT_TEMPLATE_NAME_EMPTY = 'Empty Bot'; +const BOT_TEMPLATE_NAME_SIMPLE = 'Echo Bot'; +const BOT_TEMPLATE_NAME_CORE = 'Core Bot'; + +const BOT_TEMPLATE_NOPROMPT_EMPTY = 'empty'; +const BOT_TEMPLATE_NOPROMPT_SIMPLE = 'echo'; +const BOT_TEMPLATE_NOPROMPT_CORE = 'core'; + +const bigBot = + ` ╭─────────────────────────────╮\n` + + ` ` + + chalk.blue.bold(`//`) + + ` ` + + chalk.blue.bold(`\\\\`) + + ` │ Welcome to the │\n` + + ` ` + + chalk.blue.bold(`//`) + + ` () () ` + + chalk.blue.bold(`\\\\`) + + ` │ Microsoft Java Bot Builder │\n` + + ` ` + + chalk.blue.bold(`\\\\`) + + ` ` + + chalk.blue.bold(`//`) + + ` /│ generator! │\n` + + ` ` + + chalk.blue.bold(`\\\\`) + + ` ` + + chalk.blue.bold(`//`) + + ` ╰─────────────────────────────╯\n` + + ` v${pkg.version}`; + +const tinyBot = + ` ` + chalk.blue.bold(`<`) + ` ** ` + chalk.blue.bold(`>`) + ` `; + +module.exports = class extends Generator { + constructor(args, opts) { + super(args, opts); + + // allocate an object that we can use to store our user prompt values from our askFor* functions + this.templateConfig = {}; + + // configure the commandline options + this._configureCommandlineOptions(); + } + + initializing() { + // give the user some data before we start asking them questions + this.log(bigBot); + } + + prompting() { + // if we're told to not prompt, then pick what we need and return + if (this.options.noprompt) { + // this function will throw if it encounters errors/invalid options + return this._verifyNoPromptOptions(); + } + + const userPrompts = this._getPrompts(); + async function executePrompts([prompt, ...rest]) { + if (prompt) { + await prompt(); + return executePrompts(rest); + } + } + + return executePrompts(userPrompts); + } + + writing() { + // if the user confirmed their settings, then lets go ahead + // an install module dependencies + if (this.templateConfig.finalConfirmation === true) { + const botName = this.templateConfig.botName; + const packageName = this.templateConfig.packageName.replace(/-/g, '_').toLowerCase(); + const packageTree = packageName.replace(/\./g, '/'); + const artifact = _.kebabCase(this.templateConfig.botName).replace(/([^a-z0-9-]+)/gi, ``); + const directoryName = _.camelCase(this.templateConfig.botName); + const template = this.templateConfig.template.toLowerCase(); + + if (path.basename(this.destinationPath()) !== directoryName) { + mkdirp.sync(directoryName); + this.destinationRoot(this.destinationPath(directoryName)); + } + + // Copy the project tree + this.fs.copyTpl( + this.templatePath(path.join(template, 'project', '**')), + this.destinationPath(), + { + botName, + packageName, + artifact + } + ); + + // Copy main source + this.fs.copyTpl( + this.templatePath(path.join(template, 'src/main/java/**')), + this.destinationPath(path.join('src/main/java', packageTree)), + { + packageName + } + ); + + // Copy test source + this.fs.copyTpl( + this.templatePath(path.join(template, 'src/test/java/**')), + this.destinationPath(path.join('src/test/java', packageTree)), + { + packageName + } + ); + } + } + + end() { + if (this.templateConfig.finalConfirmation === true) { + this.log(chalk.green('------------------------ ')); + this.log(chalk.green(' Your new bot is ready! ')); + this.log(chalk.green('------------------------ ')); + this.log(`Your bot should be in a directory named "${_.camelCase(this.templateConfig.botName)}"`); + this.log('Open the ' + chalk.green.bold('README.md') + ' to learn how to run your bot. '); + this.log('Thank you for using the Microsoft Bot Framework. '); + this.log(`\n${tinyBot} The Bot Framework Team`); + } else { + this.log(chalk.red.bold('-------------------------------- ')); + this.log(chalk.red.bold(' New bot creation was canceled. ')); + this.log(chalk.red.bold('-------------------------------- ')); + this.log('Thank you for using the Microsoft Bot Framework. '); + this.log(`\n${tinyBot} The Bot Framework Team`); + } + } + + _configureCommandlineOptions() { + this.option('botName', { + desc: 'The name you want to give to your bot', + type: String, + default: 'echo', + alias: 'N' + }); + + this.option('packageName', { + desc: `What's the fully qualified package name of your bot?`, + type: String, + default: 'com.mycompany.echo', + alias: 'P' + }); + + const templateDesc = `The initial bot capabilities. (${BOT_TEMPLATE_NAME_EMPTY} | ${BOT_TEMPLATE_NAME_SIMPLE} | ${BOT_TEMPLATE_NAME_CORE})`; + this.option('template', { + desc: templateDesc, + type: String, + default: BOT_TEMPLATE_NAME_SIMPLE, + alias: 'T' + }); + + this.argument('noprompt', { + desc: 'Do not prompt for any information or confirmation', + type: Boolean, + required: false, + default: false + }); + } + + _getPrompts() { + return [ + // ask the user to name their bot + async () => { + return this.prompt({ + type: 'input', + name: 'botName', + message: `What's the name of your bot?`, + default: (this.options.botName ? this.options.botName : 'echo') + }).then(answer => { + // store the botname description answer + this.templateConfig.botName = answer.botName; + }); + }, + + // ask for package name + async () => { + return this.prompt({ + type: 'input', + name: 'packageName', + message: `What's the fully qualified package name of your bot?`, + default: (this.options.packageName ? this.options.packageName : 'com.mycompany.echo') + }).then(answer => { + // store the package name description answer + this.templateConfig.packageName = answer.packageName; + }); + }, + + + // ask the user which bot template we should use + async () => { + return this.prompt({ + type: 'list', + name: 'template', + message: 'Which template would you like to start with?', + choices: [ + { + name: BOT_TEMPLATE_NAME_SIMPLE, + value: BOT_TEMPLATE_NOPROMPT_SIMPLE + }, + { + name: BOT_TEMPLATE_NAME_EMPTY, + value: BOT_TEMPLATE_NOPROMPT_EMPTY + }, + { + name: BOT_TEMPLATE_NAME_CORE, + value: BOT_TEMPLATE_NOPROMPT_CORE + } + ], + default: (this.options.template ? _.toLower(this.options.template) : BOT_TEMPLATE_NOPROMPT_SIMPLE) + }).then(answer => { + // store the template prompt answer + this.templateConfig.template = answer.template; + }); + }, + + // ask the user for final confirmation before we generate their bot + async () => { + return this.prompt({ + type: 'confirm', + name: 'finalConfirmation', + message: 'Looking good. Shall I go ahead and create your new bot?', + default: true + }).then(answer => { + // store the finalConfirmation prompt answer + this.templateConfig.finalConfirmation = answer.finalConfirmation; + }); + } + ]; + } + + // if we're run with the --noprompt option, verify that all required options were supplied. + // throw for missing options, or a resolved Promise + _verifyNoPromptOptions() { + this.templateConfig = _.pick(this.options, ['botName', 'packageName', 'template']) + + // validate we have what we need, or we'll need to throw + if (!this.templateConfig.botName) { + throw new Error('Must specify a name for your bot when using --noprompt argument. Use --botName or -N'); + } + if (!this.templateConfig.packageName) { + throw new Error('Must specify a package name for your bot when using --noprompt argument. Use --packageName or -P'); + } + + // make sure we have a supported template + const template = (this.templateConfig.template ? _.toLower(this.templateConfig.template) : undefined); + const tmplEmpty = _.toLower(BOT_TEMPLATE_NOPROMPT_EMPTY); + const tmplSimple = _.toLower(BOT_TEMPLATE_NOPROMPT_SIMPLE); + const tmplCore = _.toLower(BOT_TEMPLATE_NOPROMPT_CORE); + if (!template || (template !== tmplEmpty && template !== tmplSimple && template !== tmplCore)) { + throw new Error('Must specify a template when using --noprompt argument. Use --template or -T'); + } + + // when run using --noprompt and we have all the required templateConfig, then set final confirmation to true + // so we can go forward and create the new bot without prompting the user for confirmation + this.templateConfig.finalConfirmation = true; + + return Promise.resolve(); + } +}; diff --git a/generators/generators/app/templates/core/project/README-LUIS.md b/generators/generators/app/templates/core/project/README-LUIS.md new file mode 100644 index 000000000..12bc78ed0 --- /dev/null +++ b/generators/generators/app/templates/core/project/README-LUIS.md @@ -0,0 +1,216 @@ +# Setting up LUIS via CLI: + +This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. + +> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ +> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ +> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ + + [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app + [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app + +## Table of Contents: + +- [Prerequisites](#Prerequisites) +- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) +- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) + +___ + +## [Prerequisites](#Table-of-Contents): + +#### Install Azure CLI >=2.0.61: + +Visit the following page to find the correct installer for your OS: +- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest + +#### Install LUIS CLI >=2.4.0: + +Open a CLI of your choice and type the following: + +```bash +npm i -g luis-apis@^2.4.0 +``` + +#### LUIS portal account: + +You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. + +After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. + + [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] + [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key + +___ + +## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) + +### 1. Import the local LUIS application to luis.ai + +```bash +luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" +``` + +Outputs the following JSON: + +```json +{ + "id": "########-####-####-####-############", + "name": "FlightBooking", + "description": "A LUIS model that uses intent and entities.", + "culture": "en-us", + "usageScenario": "", + "domain": "", + "versionsCount": 1, + "createdDateTime": "2019-03-29T18:32:02Z", + "endpoints": {}, + "endpointHitsCount": 0, + "activeVersion": "0.1", + "ownerEmail": "bot@contoso.com", + "tokenizerVersion": "1.0.0" +} +``` + +For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. + +### 2. Train the LUIS Application + +```bash +luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait +``` + +### 3. Publish the LUIS Application + +```bash +luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" +``` + +> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
+> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
+> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
+> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. + + [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 + +Outputs the following: + +```json + { + "versionId": "0.1", + "isStaging": false, + "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", + "region": "westus", + "assignedEndpointKey": null, + "endpointRegion": "westus", + "failedRegions": "", + "publishedDateTime": "2019-03-29T18:40:32Z", + "directVersionPublish": false +} +``` + +To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. + + [README-LUIS]: ./README-LUIS.md + +___ + +## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) + +### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI + +> _Note:_
+> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ +> ```bash +> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" +> ``` +> _To see a list of valid locations, use `az account list-locations`_ + + +```bash +# Use Azure CLI to create the LUIS Key resource on Azure +az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" +``` + +The command will output a response similar to the JSON below: + +```json +{ + "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", + "etag": "\"########-####-####-####-############\"", + "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", + "internalId": "################################", + "kind": "luis", + "location": "westus", + "name": "NewLuisResourceName", + "provisioningState": "Succeeded", + "resourceGroup": "ResourceGroupName", + "sku": { + "name": "S0", + "tier": null + }, + "tags": null, + "type": "Microsoft.CognitiveServices/accounts" +} +``` + + + +Take the output from the previous command and create a JSON file in the following format: + +```json +{ + "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "ResourceGroupName", + "accountName": "NewLuisResourceName" +} +``` + +### 2. Retrieve ARM access token via Azure CLI + +```bash +az account get-access-token --subscription "AzureSubscriptionGuid" +``` + +This will return an object that looks like this: + +```json +{ + "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", + "expiresOn": "2200-12-31 23:59:59.999999", + "subscription": "AzureSubscriptionGuid", + "tenant": "tenant-guid", + "tokenType": "Bearer" +} +``` + +The value needed for the next step is the `"accessToken"`. + +### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application + +```bash +luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" +``` + +If successful, it should yield a response like this: + +```json +{ + "code": "Success", + "message": "Operation Successful" +} +``` + +### 4. See the LUIS Cognitive Services' keys + +```bash +az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" +``` + +This will return an object that looks like this: + +```json +{ + "key1": "9a69####dc8f####8eb4####399f####", + "key2": "####f99e####4b1a####fb3b####6b9f" +} +``` diff --git a/generators/generators/app/templates/core/project/README.md b/generators/generators/app/templates/core/project/README.md new file mode 100644 index 000000000..5428395fc --- /dev/null +++ b/generators/generators/app/templates/core/project/README.md @@ -0,0 +1,67 @@ +# <%= botName %> + +Bot Framework v4 core bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: + +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities +- Implement a multi-turn conversation using Dialogs +- Handle user interruptions for such things as `Help` or `Cancel` +- Prompt for and validate requests for information from the user + +## Prerequisites + +This sample **requires** prerequisites in order to run. + +### Overview + +This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. + +### Create a LUIS Application to enable language understanding + +The LUIS model for this example can be found under `cognitiveModels/FlightBooking.json` and the LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). + +Once you created the LUIS model, update `application.properties` with your `LuisAppId`, `LuisAPIKey` and `LuisAPIHostName`. + +``` + LuisAppId="Your LUIS App Id" + LuisAPIKey="Your LUIS Subscription key here" + LuisAPIHostName="Your LUIS App region here (i.e: westus.api.cognitive.microsoft.com)" +``` + +## To try this sample + +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\<%= artifact %>-1.0.0.jar` + +## Testing the bot using Bot Framework Emulator + +[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the latest Bot Framework Emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- File -> Open Bot +- Enter a Bot URL of `http://localhost:3978/api/messages` + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Spring Boot](https://spring.io/projects/spring-boot) diff --git a/generators/generators/app/templates/core/project/cognitiveModels/FlightBooking.json b/generators/generators/app/templates/core/project/cognitiveModels/FlightBooking.json new file mode 100644 index 000000000..603dd06b2 --- /dev/null +++ b/generators/generators/app/templates/core/project/cognitiveModels/FlightBooking.json @@ -0,0 +1,339 @@ +{ + "luis_schema_version": "3.2.0", + "versionId": "0.1", + "name": "FlightBooking", + "desc": "Luis Model for <%= botName %>", + "culture": "en-us", + "tokenizerVersion": "1.0.0", + "intents": [ + { + "name": "BookFlight" + }, + { + "name": "Cancel" + }, + { + "name": "GetWeather" + }, + { + "name": "None" + } + ], + "entities": [], + "composites": [ + { + "name": "From", + "children": [ + "Airport" + ], + "roles": [] + }, + { + "name": "To", + "children": [ + "Airport" + ], + "roles": [] + } + ], + "closedLists": [ + { + "name": "Airport", + "subLists": [ + { + "canonicalForm": "Paris", + "list": [ + "paris", + "cdg" + ] + }, + { + "canonicalForm": "London", + "list": [ + "london", + "lhr" + ] + }, + { + "canonicalForm": "Berlin", + "list": [ + "berlin", + "txl" + ] + }, + { + "canonicalForm": "New York", + "list": [ + "new york", + "jfk" + ] + }, + { + "canonicalForm": "Seattle", + "list": [ + "seattle", + "sea" + ] + } + ], + "roles": [] + } + ], + "patternAnyEntities": [], + "regex_entities": [], + "prebuiltEntities": [ + { + "name": "datetimeV2", + "roles": [] + } + ], + "model_features": [], + "regex_features": [], + "patterns": [], + "utterances": [ + { + "text": "book a flight", + "intent": "BookFlight", + "entities": [] + }, + { + "text": "book a flight from new york", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 19, + "endPos": 26 + } + ] + }, + { + "text": "book a flight from seattle", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 19, + "endPos": 25 + } + ] + }, + { + "text": "book a hotel in new york", + "intent": "None", + "entities": [] + }, + { + "text": "book a restaurant", + "intent": "None", + "entities": [] + }, + { + "text": "book flight from london to paris on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 17, + "endPos": 22 + }, + { + "entity": "To", + "startPos": 27, + "endPos": 31 + } + ] + }, + { + "text": "book flight to berlin on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 15, + "endPos": 20 + } + ] + }, + { + "text": "book me a flight from london to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 22, + "endPos": 27 + }, + { + "entity": "To", + "startPos": 32, + "endPos": 36 + } + ] + }, + { + "text": "bye", + "intent": "Cancel", + "entities": [] + }, + { + "text": "cancel booking", + "intent": "Cancel", + "entities": [] + }, + { + "text": "exit", + "intent": "Cancel", + "entities": [] + }, + { + "text": "find an airport near me", + "intent": "None", + "entities": [] + }, + { + "text": "flight to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "flight to paris from london on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + }, + { + "entity": "From", + "startPos": 21, + "endPos": 26 + } + ] + }, + { + "text": "fly from berlin to paris on may 5th", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 9, + "endPos": 14 + }, + { + "entity": "To", + "startPos": 19, + "endPos": 23 + } + ] + }, + { + "text": "go to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 6, + "endPos": 10 + } + ] + }, + { + "text": "going from paris to berlin", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 11, + "endPos": 15 + }, + { + "entity": "To", + "startPos": 20, + "endPos": 25 + } + ] + }, + { + "text": "i'd like to rent a car", + "intent": "None", + "entities": [] + }, + { + "text": "ignore", + "intent": "Cancel", + "entities": [] + }, + { + "text": "travel from new york to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 12, + "endPos": 19 + }, + { + "entity": "To", + "startPos": 24, + "endPos": 28 + } + ] + }, + { + "text": "travel to new york", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 17 + } + ] + }, + { + "text": "travel to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "what's the forecast for this friday?", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like for tomorrow", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like in new york", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like?", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "winter is coming", + "intent": "None", + "entities": [] + } + ], + "settings": [] +} diff --git a/generators/generators/app/templates/core/project/deploymentTemplates/template-with-new-rg.json b/generators/generators/app/templates/core/project/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..196cfb933 --- /dev/null +++ b/generators/generators/app/templates/core/project/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,292 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "azurebot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} diff --git a/generators/generators/app/templates/core/project/deploymentTemplates/template-with-preexisting-rg.json b/generators/generators/app/templates/core/project/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..d6feb0a0f --- /dev/null +++ b/generators/generators/app/templates/core/project/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,260 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "azurebot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} diff --git a/generators/generators/app/templates/core/project/pom.xml b/generators/generators/app/templates/core/project/pom.xml new file mode 100644 index 000000000..8306e22f8 --- /dev/null +++ b/generators/generators/app/templates/core/project/pom.xml @@ -0,0 +1,254 @@ + + + + 4.0.0 + + <%= packageName %> + <%= artifact %> + 1.0.0 + jar + + ${project.groupId}:${project.artifactId} + This package contains a Java Core Bot sample using Spring Boot. + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 2.4.0 + + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + 1.8 + 1.8 + 1.8 + <%= packageName %>.Application + + + + + org.springframework.boot + spring-boot-starter-test + 2.4.0 + test + + + junit + junit + 4.13.1 + test + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + org.apache.logging.log4j + log4j-core + 2.17.1 + + + org.apache.logging.log4j + log4j-to-slf4j + 2.15.0 + test + + + + com.microsoft.bot + bot-integration-spring + 4.14.1 + compile + + + com.microsoft.bot + bot-dialogs + 4.14.1 + + + com.microsoft.bot + bot-ai-luis-v3 + 4.14.1 + + + + + + build + + true + + + + + src/main/resources + false + + + + + maven-compiler-plugin + 3.8.1 + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + <%= packageName %>.Application + + + + + + com.microsoft.azure + azure-webapp-maven-plugin + 1.12.0 + + V2 + ${groupname} + ${botname} + + + JAVA_OPTS + -Dserver.port=80 + + + + linux + Java 8 + Java SE + + + + + ${project.basedir}/target + + *.jar + + + + + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + true + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + false + + + + attach-javadocs + + jar + + + + + + + + + diff --git a/generators/generators/app/templates/core/project/src/main/resources/application.properties b/generators/generators/app/templates/core/project/src/main/resources/application.properties new file mode 100644 index 000000000..255d7cd56 --- /dev/null +++ b/generators/generators/app/templates/core/project/src/main/resources/application.properties @@ -0,0 +1,6 @@ +MicrosoftAppId= +MicrosoftAppPassword= +LuisAppId= +LuisAPIKey= +LuisAPIHostName= +server.port=3978 diff --git a/generators/generators/app/templates/core/project/src/main/resources/cards/welcomeCard.json b/generators/generators/app/templates/core/project/src/main/resources/cards/welcomeCard.json new file mode 100644 index 000000000..9b6389e39 --- /dev/null +++ b/generators/generators/app/templates/core/project/src/main/resources/cards/welcomeCard.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "Image", + "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "size": "stretch" + }, + { + "type": "TextBlock", + "spacing": "medium", + "size": "default", + "weight": "bolder", + "text": "Welcome to Bot Framework!", + "wrap": true, + "maxLines": 0 + }, + { + "type": "TextBlock", + "size": "default", + "isSubtle": true, + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get an overview", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Ask a question", + "url": "https://stackoverflow.com/questions/tagged/botframework" + }, + { + "type": "Action.OpenUrl", + "title": "Learn how to deploy", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] +} diff --git a/generators/generators/app/templates/core/project/src/main/resources/log4j2.json b/generators/generators/app/templates/core/project/src/main/resources/log4j2.json new file mode 100644 index 000000000..67c0ad530 --- /dev/null +++ b/generators/generators/app/templates/core/project/src/main/resources/log4j2.json @@ -0,0 +1,18 @@ +{ + "configuration": { + "name": "Default", + "appenders": { + "Console": { + "name": "Console-Appender", + "target": "SYSTEM_OUT", + "PatternLayout": {"pattern": "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"} + } + }, + "loggers": { + "root": { + "level": "debug", + "appender-ref": {"ref": "Console-Appender","level": "debug"} + } + } + } +} diff --git a/generators/generators/app/templates/core/project/src/main/webapp/META-INF/MANIFEST.MF b/generators/generators/app/templates/core/project/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/generators/generators/app/templates/core/project/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/generators/generators/app/templates/core/project/src/main/webapp/WEB-INF/web.xml b/generators/generators/app/templates/core/project/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/generators/generators/app/templates/core/project/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + dispatcher + + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + /WEB-INF/spring/dispatcher-config.xml + + 1 + \ No newline at end of file diff --git a/generators/generators/app/templates/core/project/src/main/webapp/index.html b/generators/generators/app/templates/core/project/src/main/webapp/index.html new file mode 100644 index 000000000..593ac520c --- /dev/null +++ b/generators/generators/app/templates/core/project/src/main/webapp/index.html @@ -0,0 +1,417 @@ + + + + + + + <%= botName %> + + + + + +
+
+
+
<%= botName %>
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + diff --git a/generators/generators/app/templates/core/src/main/java/Application.java b/generators/generators/app/templates/core/src/main/java/Application.java new file mode 100644 index 000000000..7f99e415d --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/Application.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.integration.AdapterWithErrorHandler; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.integration.spring.BotController; +import com.microsoft.bot.integration.spring.BotDependencyConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * This is the starting point of the Sprint Boot Bot application. + */ +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// org.springframework.web.bind.annotation.RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +/** + * This class extends the BotDependencyConfiguration which provides the default + * implementations for a Bot application. The Application class should + * override methods in order to provide custom implementations. + */ +public class Application extends BotDependencyConfiguration { + + /** + * The start method. + * + * @param args The args. + */ + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * Returns the Bot for this application. + * + *

+ * The @Component annotation could be used on the Bot class instead of this method with the + * @Bean annotation. + *

+ * + * @return The Bot implementation for this application. + */ + @Bean + public Bot getBot( + Configuration configuration, + UserState userState, + ConversationState conversationState + ) { + FlightBookingRecognizer recognizer = new FlightBookingRecognizer(configuration); + MainDialog dialog = new MainDialog(recognizer, new BookingDialog()); + return new DialogAndWelcomeBot<>(conversationState, userState, dialog); + } + + /** + * Returns a custom Adapter that provides error handling. + * + * @param configuration The Configuration object to use. + * @return An error handling BotFrameworkHttpAdapter. + */ + @Override + public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) { + return new AdapterWithErrorHandler(configuration); + } +} + diff --git a/generators/generators/app/templates/core/src/main/java/BookingDetails.java b/generators/generators/app/templates/core/src/main/java/BookingDetails.java new file mode 100644 index 000000000..160242106 --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/BookingDetails.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +/** + * The model class to retrieve the information of the booking. + */ +public class BookingDetails { + + private String destination; + private String origin; + private String travelDate; + + /** + * Gets the destination of the booking. + * + * @return The destination. + */ + public String getDestination() { + return destination; + } + + /** + * Sets the destination of the booking. + * + * @param withDestination The new destination. + */ + public void setDestination(String withDestination) { + this.destination = withDestination; + } + + /** + * Gets the origin of the booking. + * + * @return The origin. + */ + public String getOrigin() { + return origin; + } + + /** + * Sets the origin of the booking. + * + * @param withOrigin The new origin. + */ + public void setOrigin(String withOrigin) { + this.origin = withOrigin; + } + + /** + * Gets the travel date of the booking. + * + * @return The travel date. + */ + public String getTravelDate() { + return travelDate; + } + + /** + * Sets the travel date of the booking. + * + * @param withTravelDate The new travel date. + */ + public void setTravelDate(String withTravelDate) { + this.travelDate = withTravelDate; + } +} diff --git a/generators/generators/app/templates/core/src/main/java/BookingDialog.java b/generators/generators/app/templates/core/src/main/java/BookingDialog.java new file mode 100644 index 000000000..d5e7322b3 --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/BookingDialog.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.prompts.ConfirmPrompt; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.TextPrompt; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.recognizers.datatypes.timex.expression.Constants; +import com.microsoft.recognizers.datatypes.timex.expression.TimexProperty; + +/** + * The class containing the booking dialogs. + */ +public class BookingDialog extends CancelAndHelpDialog { + + private final String destinationStepMsgText = "Where would you like to travel to?"; + private final String originStepMsgText = "Where are you traveling from?"; + + /** + * The constructor of the Booking Dialog class. + */ + public BookingDialog() { + super("BookingDialog"); + + addDialog(new TextPrompt("TextPrompt")); + addDialog(new ConfirmPrompt("ConfirmPrompt")); + addDialog(new DateResolverDialog(null)); + WaterfallStep[] waterfallSteps = { + this::destinationStep, + this::originStep, + this::travelDateStep, + this::confirmStep, + this::finalStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture destinationStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + if (bookingDetails.getDestination() == null || bookingDetails.getDestination().trim().isEmpty()) { + Activity promptMessage = + MessageFactory.text(destinationStepMsgText, destinationStepMsgText, + InputHints.EXPECTING_INPUT + ); + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + return stepContext.prompt("TextPrompt", promptOptions); + } + + return stepContext.next(bookingDetails.getDestination()); + } + + private CompletableFuture originStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + bookingDetails.setDestination((String) stepContext.getResult()); + + if (bookingDetails.getOrigin() == null || bookingDetails.getOrigin().trim().isEmpty()) { + Activity promptMessage = + MessageFactory + .text(originStepMsgText, originStepMsgText, InputHints.EXPECTING_INPUT); + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + return stepContext.prompt("TextPrompt", promptOptions); + } + + return stepContext.next(bookingDetails.getOrigin()); + } + + private CompletableFuture travelDateStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + bookingDetails.setOrigin((String) stepContext.getResult()); + + if (bookingDetails.getTravelDate() == null || isAmbiguous(bookingDetails.getTravelDate())) { + return stepContext.beginDialog("DateResolverDialog", bookingDetails.getTravelDate()); + } + + return stepContext.next(bookingDetails.getTravelDate()); + } + + private CompletableFuture confirmStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + bookingDetails.setTravelDate((String) stepContext.getResult()); + + String messageText = String.format( + "Please confirm, I have you traveling to: %s from: %s on: %s. Is this correct?", + bookingDetails.getDestination(), bookingDetails.getOrigin(), bookingDetails.getTravelDate()); + Activity promptMessage = MessageFactory.text(messageText, messageText, InputHints.EXPECTING_INPUT); + + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + + return stepContext.prompt("ConfirmPrompt", promptOptions); + } + + + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + if ((Boolean) stepContext.getResult()) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + return stepContext.endDialog(bookingDetails); + } + + return stepContext.endDialog(null); + } + + private static boolean isAmbiguous(String timex) { + TimexProperty timexProperty = new TimexProperty(timex); + return !timexProperty.getTypes().contains(Constants.TimexTypes.DEFINITE); + } +} diff --git a/generators/generators/app/templates/core/src/main/java/CancelAndHelpDialog.java b/generators/generators/app/templates/core/src/main/java/CancelAndHelpDialog.java new file mode 100644 index 000000000..209b9b3b0 --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/CancelAndHelpDialog.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.ComponentDialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.DialogTurnStatus; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.InputHints; + +import java.util.concurrent.CompletableFuture; + +/** + * The class in charge of the dialog interruptions. + */ +public class CancelAndHelpDialog extends ComponentDialog { + + private final String helpMsgText = "Show help here"; + private final String cancelMsgText = "Cancelling..."; + + /** + * The constructor of the CancelAndHelpDialog class. + * + * @param id The dialog's Id. + */ + public CancelAndHelpDialog(String id) { + super(id); + } + + /** + * Called when the dialog is _continued_, where it is the active dialog and the user replies + * with a new activity. + * + * @param innerDc innerDc The inner {@link DialogContext} for the current turn of conversation. + * @return A {@link CompletableFuture} representing the asynchronous operation. If the task is + * successful, the result indicates whether the dialog is still active after the turn has been + * processed by the dialog. The result may also contain a return value. + */ + @Override + protected CompletableFuture onContinueDialog(DialogContext innerDc) { + return interrupt(innerDc).thenCompose(result -> { + if (result != null) { + return CompletableFuture.completedFuture(result); + } + return super.onContinueDialog(innerDc); + }); + } + + private CompletableFuture interrupt(DialogContext innerDc) { + if (innerDc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + String text = innerDc.getContext().getActivity().getText().toLowerCase(); + + switch (text) { + case "help": + case "?": + Activity helpMessage = MessageFactory + .text(helpMsgText, helpMsgText, InputHints.EXPECTING_INPUT); + return innerDc.getContext().sendActivity(helpMessage) + .thenCompose(sendResult -> + CompletableFuture + .completedFuture(new DialogTurnResult(DialogTurnStatus.WAITING))); + case "cancel": + case "quit": + Activity cancelMessage = MessageFactory + .text(cancelMsgText, cancelMsgText, InputHints.IGNORING_INPUT); + return innerDc.getContext() + .sendActivity(cancelMessage) + .thenCompose(sendResult -> innerDc.cancelAllDialogs()); + default: + break; + } + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/generators/generators/app/templates/core/src/main/java/DateResolverDialog.java b/generators/generators/app/templates/core/src/main/java/DateResolverDialog.java new file mode 100644 index 000000000..687eb45ac --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/DateResolverDialog.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.prompts.DateTimePrompt; +import com.microsoft.bot.dialogs.prompts.DateTimeResolution; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.PromptValidatorContext; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.recognizers.datatypes.timex.expression.Constants; +import com.microsoft.recognizers.datatypes.timex.expression.TimexProperty; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * The class containing the date resolver dialogs. + */ +public class DateResolverDialog extends CancelAndHelpDialog { + private final String promptMsgText = "When would you like to travel?"; + private final String repromptMsgText = + "I'm sorry, to make your booking please enter a full travel date including Day Month and Year."; + + + /** + * The constructor of the DateResolverDialog class. + * @param id The dialog's id. + */ + public DateResolverDialog(@Nullable String id) { + super(id != null ? id : "DateResolverDialog"); + + + addDialog(new DateTimePrompt("DateTimePrompt", + DateResolverDialog::dateTimePromptValidator, null)); + WaterfallStep[] waterfallSteps = { + this::initialStep, + this::finalStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture initialStep(WaterfallStepContext stepContext) { + String timex = (String) stepContext.getOptions(); + + Activity promptMessage = MessageFactory.text(promptMsgText, promptMsgText, InputHints.EXPECTING_INPUT); + Activity repromptMessage = MessageFactory.text(repromptMsgText, repromptMsgText, InputHints.EXPECTING_INPUT); + + if (timex == null) { + // We were not given any date at all so prompt the user. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + promptOptions.setRetryPrompt(repromptMessage); + return stepContext.prompt("DateTimePrompt", promptOptions); + } + + // We have a Date we just need to check it is unambiguous. + TimexProperty timexProperty = new TimexProperty(timex); + if (!timexProperty.getTypes().contains(Constants.TimexTypes.DEFINITE)) { + // This is essentially a "reprompt" of the data we were given up front. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(repromptMessage); + return stepContext.prompt("DateTimePrompt", promptOptions); + } + + DateTimeResolution dateTimeResolution = new DateTimeResolution(); + dateTimeResolution.setTimex(timex); + List dateTimeResolutions = new ArrayList(); + dateTimeResolutions.add(dateTimeResolution); + return stepContext.next(dateTimeResolutions); + } + + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + String timex = ((ArrayList) stepContext.getResult()).get(0).getTimex(); + return stepContext.endDialog(timex); + } + + private static CompletableFuture dateTimePromptValidator( + PromptValidatorContext> promptContext + ) { + if (promptContext.getRecognized().getSucceeded()) { + // This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the + // Time part. TIMEX is a format that represents DateTime expressions that include some ambiguity. + // e.g. missing a Year. + String timex = ((List) promptContext.getRecognized().getValue()) + .get(0).getTimex().split("T")[0]; + + // If this is a definite Date including year, month and day we are good otherwise reprompt. + // A better solution might be to let the user know what part is actually missing. + Boolean isDefinite = new TimexProperty(timex).getTypes().contains(Constants.TimexTypes.DEFINITE); + + return CompletableFuture.completedFuture(isDefinite); + } + + return CompletableFuture.completedFuture(false); + } +} diff --git a/generators/generators/app/templates/core/src/main/java/DialogAndWelcomeBot.java b/generators/generators/app/templates/core/src/main/java/DialogAndWelcomeBot.java new file mode 100644 index 000000000..13143ced2 --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/DialogAndWelcomeBot.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.Serialization; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * The class containing the welcome dialog. + * + * @param is a Dialog. + */ +public class DialogAndWelcomeBot extends DialogBot { + + /** + * Creates a DialogBot. + * + * @param withConversationState ConversationState to use in the bot + * @param withUserState UserState to use + * @param withDialog Param inheriting from Dialog class + */ + public DialogAndWelcomeBot( + ConversationState withConversationState, UserState withUserState, T withDialog + ) { + super(withConversationState, withUserState, withDialog); + } + + /** + * When the {@link #onConversationUpdateActivity(TurnContext)} method receives a conversation + * update activity that indicates one or more users other than the bot are joining the + * conversation, it calls this method. + * + * @param membersAdded A list of all the members added to the conversation, as described by the + * conversation update activity + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, TurnContext turnContext + ) { + return turnContext.getActivity().getMembersAdded().stream() + .filter(member -> !StringUtils + .equals(member.getId(), turnContext.getActivity().getRecipient().getId())) + .map(channel -> { + // Greet anyone that was not the target (recipient) of this message. + // To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. + Attachment welcomeCard = createAdaptiveCardAttachment(); + Activity response = MessageFactory + .attachment(welcomeCard, null, "Welcome to Bot Framework!", null); + + return turnContext.sendActivity(response).thenApply(sendResult -> { + return Dialog.run(getDialog(), turnContext, + getConversationState().createProperty("DialogState") + ); + }); + }) + .collect(CompletableFutures.toFutureList()) + .thenApply(resourceResponse -> null); + } + + // Load attachment from embedded resource. + private Attachment createAdaptiveCardAttachment() { + try ( + InputStream inputStream = Thread.currentThread(). + getContextClassLoader().getResourceAsStream("cards/welcomeCard.json") + ) { + String adaptiveCardJson = IOUtils + .toString(inputStream, StandardCharsets.UTF_8.toString()); + + Attachment attachment = new Attachment(); + attachment.setContentType("application/vnd.microsoft.card.adaptive"); + attachment.setContent(Serialization.jsonToTree(adaptiveCardJson)); + return attachment; + + } catch (IOException e) { + e.printStackTrace(); + return new Attachment(); + } + } +} diff --git a/generators/generators/app/templates/core/src/main/java/DialogBot.java b/generators/generators/app/templates/core/src/main/java/DialogBot.java new file mode 100644 index 000000000..02b74135c --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/DialogBot.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotState; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.dialogs.Dialog; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +/** + * This Bot implementation can run any type of Dialog. The use of type parameterization is to allow + * multiple different bots to be run at different endpoints within the same project. This can be + * achieved by defining distinct Controller types each with dependency on distinct Bot types. The + * ConversationState is used by the Dialog system. The UserState isn't, however, it might have been + * used in a Dialog implementation, and the requirement is that all BotState objects are saved at + * the end of a turn. + * + * @param parameter of a type inheriting from Dialog + */ +public class DialogBot extends ActivityHandler { + + private Dialog dialog; + private BotState conversationState; + private BotState userState; + + /** + * Gets the dialog in use. + * + * @return instance of dialog + */ + protected Dialog getDialog() { + return dialog; + } + + /** + * Gets the conversation state. + * + * @return instance of conversationState + */ + protected BotState getConversationState() { + return conversationState; + } + + /** + * Gets the user state. + * + * @return instance of userState + */ + protected BotState getUserState() { + return userState; + } + + /** + * Sets the dialog in use. + * + * @param withDialog the dialog (of Dialog type) to be set + */ + protected void setDialog(Dialog withDialog) { + dialog = withDialog; + } + + /** + * Sets the conversation state. + * + * @param withConversationState the conversationState (of BotState type) to be set + */ + protected void setConversationState(BotState withConversationState) { + conversationState = withConversationState; + } + + /** + * Sets the user state. + * + * @param withUserState the userState (of BotState type) to be set + */ + protected void setUserState(BotState withUserState) { + userState = withUserState; + } + + /** + * Creates a DialogBot. + * + * @param withConversationState ConversationState to use in the bot + * @param withUserState UserState to use + * @param withDialog Param inheriting from Dialog class + */ + public DialogBot( + ConversationState withConversationState, UserState withUserState, T withDialog + ) { + this.conversationState = withConversationState; + this.userState = withUserState; + this.dialog = withDialog; + } + + /** + * Saves the BotState objects at the end of each turn. + * + * @param turnContext + * @return + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext) { + return super.onTurn(turnContext) + .thenCompose(turnResult -> conversationState.saveChanges(turnContext, false)) + .thenCompose(saveResult -> userState.saveChanges(turnContext, false)); + } + + /** + * This method is executed when the turnContext receives a message activity. + * + * @param turnContext + * @return + */ + @Override + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + LoggerFactory.getLogger(DialogBot.class).info("Running dialog with Message Activity."); + + // Run the Dialog with the new message Activity. + return Dialog.run(dialog, turnContext, conversationState.createProperty("DialogState")); + } +} diff --git a/generators/generators/app/templates/core/src/main/java/FlightBookingRecognizer.java b/generators/generators/app/templates/core/src/main/java/FlightBookingRecognizer.java new file mode 100644 index 000000000..9d328e1b0 --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/FlightBookingRecognizer.java @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.ai.luis.LuisApplication; +import com.microsoft.bot.ai.luis.LuisRecognizer; +import com.microsoft.bot.ai.luis.LuisRecognizerOptionsV3; +import com.microsoft.bot.builder.Recognizer; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.integration.Configuration; +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.CompletableFuture; + +/** + * The class in charge of recognizing the booking information. + */ +public class FlightBookingRecognizer implements Recognizer { + + private LuisRecognizer recognizer; + + /** + * The constructor of the FlightBookingRecognizer class. + * + * @param configuration The Configuration object to use. + */ + public FlightBookingRecognizer(Configuration configuration) { + Boolean luisIsConfigured = StringUtils.isNotBlank(configuration.getProperty("LuisAppId")) + && StringUtils.isNotBlank(configuration.getProperty("LuisAPIKey")) + && StringUtils.isNotBlank(configuration.getProperty("LuisAPIHostName")); + if (luisIsConfigured) { + LuisApplication luisApplication = new LuisApplication( + configuration.getProperty("LuisAppId"), + configuration.getProperty("LuisAPIKey"), + String.format("https://%s", configuration.getProperty("LuisAPIHostName")) + ); + // Set the recognizer options depending on which endpoint version you want to use. + // More details can be found in + // https://docs.microsoft.com/en-gb/azure/cognitive-services/luis/luis-migration-api-v3 + LuisRecognizerOptionsV3 recognizerOptions = new LuisRecognizerOptionsV3(luisApplication); + recognizerOptions.setIncludeInstanceData(true); + + this.recognizer = new LuisRecognizer(recognizerOptions); + } + } + + /** + * Verify if the recognizer is configured. + * + * @return True if it's configured, False if it's not. + */ + public Boolean isConfigured() { + return this.recognizer != null; + } + + /** + * Return an object with preformatted LUIS results for the bot's dialogs to consume. + * + * @param context A {link TurnContext} + * @return A {link RecognizerResult} + */ + public CompletableFuture executeLuisQuery(TurnContext context) { + // Returns true if luis is configured in the application.properties and initialized. + return this.recognizer.recognize(context); + } + + /** + * Gets the From data from the entities which is part of the result. + * + * @param result The recognizer result. + * @return The object node representing the From data. + */ + public ObjectNode getFromEntities(RecognizerResult result) { + String fromValue = "", fromAirportValue = ""; + if (result.getEntities().get("$instance").get("From") != null) { + fromValue = result.getEntities().get("$instance").get("From").get(0).get("text") + .asText(); + } + if (!fromValue.isEmpty() + && result.getEntities().get("From").get(0).get("Airport") != null) { + fromAirportValue = result.getEntities().get("From").get(0).get("Airport").get(0).get(0) + .asText(); + } + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectNode entitiesNode = mapper.createObjectNode(); + entitiesNode.put("from", fromValue); + entitiesNode.put("airport", fromAirportValue); + return entitiesNode; + } + + /** + * Gets the To data from the entities which is part of the result. + * + * @param result The recognizer result. + * @return The object node representing the To data. + */ + public ObjectNode getToEntities(RecognizerResult result) { + String toValue = "", toAirportValue = ""; + if (result.getEntities().get("$instance").get("To") != null) { + toValue = result.getEntities().get("$instance").get("To").get(0).get("text").asText(); + } + if (!toValue.isEmpty() && result.getEntities().get("To").get(0).get("Airport") != null) { + toAirportValue = result.getEntities().get("To").get(0).get("Airport").get(0).get(0) + .asText(); + } + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectNode entitiesNode = mapper.createObjectNode(); + entitiesNode.put("to", toValue); + entitiesNode.put("airport", toAirportValue); + return entitiesNode; + } + + /** + * This value will be a TIMEX. And we are only interested in a Date so grab the first result and + * drop the Time part. TIMEX is a format that represents DateTime expressions that include some + * ambiguity. e.g. missing a Year. + * + * @param result A {link RecognizerResult} + * @return The Timex value without the Time model + */ + public String getTravelDate(RecognizerResult result) { + JsonNode datetimeEntity = result.getEntities().get("datetime"); + if (datetimeEntity == null || datetimeEntity.get(0) == null) { + return null; + } + + JsonNode timex = datetimeEntity.get(0).get("timex"); + if (timex == null || timex.get(0) == null) { + return null; + } + + String datetime = timex.get(0).asText().split("T")[0]; + return datetime; + } + + /** + * Runs an utterance through a recognizer and returns a generic recognizer result. + * + * @param turnContext Turn context. + * @return Analysis of utterance. + */ + @Override + public CompletableFuture recognize(TurnContext turnContext) { + return this.recognizer.recognize(turnContext); + } +} diff --git a/generators/generators/app/templates/core/src/main/java/MainDialog.java b/generators/generators/app/templates/core/src/main/java/MainDialog.java new file mode 100644 index 000000000..7b6cd1d8c --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/MainDialog.java @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.dialogs.ComponentDialog; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.TextPrompt; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.recognizers.datatypes.timex.expression.TimexProperty; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.lang3.StringUtils; + +/** + * The class containing the main dialog for the sample. + */ +public class MainDialog extends ComponentDialog { + + private final FlightBookingRecognizer luisRecognizer; + private final Integer plusDayValue = 7; + + /** + * The constructor of the Main Dialog class. + * + * @param withLuisRecognizer The FlightBookingRecognizer object. + * @param bookingDialog The BookingDialog object with booking dialogs. + */ + public MainDialog(FlightBookingRecognizer withLuisRecognizer, BookingDialog bookingDialog) { + super("MainDialog"); + + luisRecognizer = withLuisRecognizer; + + addDialog(new TextPrompt("TextPrompt")); + addDialog(bookingDialog); + WaterfallStep[] waterfallSteps = { + this::introStep, + this::actStep, + this::finalStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + /** + * First step in the waterfall dialog. Prompts the user for a command. Currently, this expects a + * booking request, like "book me a flight from Paris to Berlin on march 22" Note that the + * sample LUIS model will only recognize Paris, Berlin, New York and London as airport cities. + * + * @param stepContext A {@link WaterfallStepContext} + * @return A {@link DialogTurnResult} + */ + private CompletableFuture introStep(WaterfallStepContext stepContext) { + if (!luisRecognizer.isConfigured()) { + Activity text = MessageFactory.text("NOTE: LUIS is not configured. " + + "To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' " + + "to the appsettings.json file.", null, InputHints.IGNORING_INPUT); + return stepContext.getContext().sendActivity(text) + .thenCompose(sendResult -> stepContext.next(null)); + } + + // Use the text provided in FinalStepAsync or the default if it is the first time. + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy"); + String weekLaterDate = LocalDateTime.now().plusDays(plusDayValue).format(formatter); + String messageText = stepContext.getOptions() != null + ? stepContext.getOptions().toString() + : String.format("What can I help you with today?\n" + + "Say something like \"Book a flight from Paris to Berlin on %s\"", weekLaterDate); + Activity promptMessage = MessageFactory + .text(messageText, messageText, InputHints.EXPECTING_INPUT); + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + return stepContext.prompt("TextPrompt", promptOptions); + } + + /** + * Second step in the waterfall. This will use LUIS to attempt to extract the origin, + * destination and travel dates. Then, it hands off to the bookingDialog child dialog to collect + * any remaining details. + * + * @param stepContext A {@link WaterfallStepContext} + * @return A {@link DialogTurnResult} + */ + private CompletableFuture actStep(WaterfallStepContext stepContext) { + if (!luisRecognizer.isConfigured()) { + // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance. + return stepContext.beginDialog("BookingDialog", new BookingDetails()); + } + + // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) + return luisRecognizer.recognize(stepContext.getContext()).thenCompose(luisResult -> { + switch (luisResult.getTopScoringIntent().intent) { + case "BookFlight": + // Extract the values for the composite entities from the LUIS result. + ObjectNode fromEntities = luisRecognizer.getFromEntities(luisResult); + ObjectNode toEntities = luisRecognizer.getToEntities(luisResult); + + // Show a warning for Origin and Destination if we can't resolve them. + return showWarningForUnsupportedCities( + stepContext.getContext(), fromEntities, toEntities) + .thenCompose(showResult -> { + // Initialize BookingDetails with any entities we may have found in the response. + + BookingDetails bookingDetails = new BookingDetails(); + bookingDetails.setDestination(toEntities.get("airport").asText()); + bookingDetails.setOrigin(fromEntities.get("airport").asText()); + bookingDetails.setTravelDate(luisRecognizer.getTravelDate(luisResult)); + // Run the BookingDialog giving it whatever details we have from the LUIS call, + // it will fill out the remainder. + return stepContext.beginDialog("BookingDialog", bookingDetails); + } + ); + case "GetWeather": + // We haven't implemented the GetWeatherDialog so we just display a TODO message. + String getWeatherMessageText = "TODO: get weather flow here"; + Activity getWeatherMessage = MessageFactory + .text( + getWeatherMessageText, getWeatherMessageText, + InputHints.IGNORING_INPUT + ); + return stepContext.getContext().sendActivity(getWeatherMessage) + .thenCompose(resourceResponse -> stepContext.next(null)); + + default: + // Catch all for unhandled intents + String didntUnderstandMessageText = String.format( + "Sorry, I didn't get that. Please " + + " try asking in a different way (intent was %s)", + luisResult.getTopScoringIntent().intent + ); + Activity didntUnderstandMessage = MessageFactory + .text( + didntUnderstandMessageText, didntUnderstandMessageText, + InputHints.IGNORING_INPUT + ); + return stepContext.getContext().sendActivity(didntUnderstandMessage) + .thenCompose(resourceResponse -> stepContext.next(null)); + } + }); + } + + /** + * Shows a warning if the requested From or To cities are recognized as entities but they are + * not in the Airport entity list. In some cases LUIS will recognize the From and To composite + * entities as a valid cities but the From and To Airport values will be empty if those entity + * values can't be mapped to a canonical item in the Airport. + * + * @param turnContext A {@link WaterfallStepContext} + * @param fromEntities An ObjectNode with the entities of From object + * @param toEntities An ObjectNode with the entities of To object + * @return A task + */ + private static CompletableFuture showWarningForUnsupportedCities( + TurnContext turnContext, + ObjectNode fromEntities, + ObjectNode toEntities + ) { + List unsupportedCities = new ArrayList(); + + if (StringUtils.isNotBlank(fromEntities.get("from").asText()) + && StringUtils.isBlank(fromEntities.get("airport").asText())) { + unsupportedCities.add(fromEntities.get("from").asText()); + } + + if (StringUtils.isNotBlank(toEntities.get("to").asText()) + && StringUtils.isBlank(toEntities.get("airport").asText())) { + unsupportedCities.add(toEntities.get("to").asText()); + } + + if (!unsupportedCities.isEmpty()) { + String messageText = String.format( + "Sorry but the following airports are not supported: %s", + String.join(", ", unsupportedCities) + ); + Activity message = MessageFactory + .text(messageText, messageText, InputHints.IGNORING_INPUT); + return turnContext.sendActivity(message) + .thenApply(sendResult -> null); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * This is the final step in the main waterfall dialog. It wraps up the sample "book a flight" + * interaction with a simple confirmation. + * + * @param stepContext A {@link WaterfallStepContext} + * @return A {@link DialogTurnResult} + */ + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + CompletableFuture stepResult = CompletableFuture.completedFuture(null); + + // If the child dialog ("BookingDialog") was cancelled, + // the user failed to confirm or if the intent wasn't BookFlight + // the Result here will be null. + if (stepContext.getResult() instanceof BookingDetails) { + // Now we have all the booking details call the booking service. + // If the call to the booking service was successful tell the user. + BookingDetails result = (BookingDetails) stepContext.getResult(); + TimexProperty timeProperty = new TimexProperty(result.getTravelDate()); + String travelDateMsg = timeProperty.toNaturalLanguage(LocalDateTime.now()); + String messageText = String.format("I have you booked to %s from %s on %s", + result.getDestination(), result.getOrigin(), travelDateMsg + ); + Activity message = MessageFactory + .text(messageText, messageText, InputHints.IGNORING_INPUT); + stepResult = stepContext.getContext().sendActivity(message).thenApply(sendResult -> null); + } + + // Restart the main dialog with a different message the second time around + String promptMessage = "What else can I do for you?"; + return stepResult + .thenCompose(result -> stepContext.replaceDialog(getInitialDialogId(), promptMessage)); + } +} diff --git a/generators/generators/app/templates/core/src/main/java/package-info.java b/generators/generators/app/templates/core/src/main/java/package-info.java new file mode 100644 index 000000000..1a5f44b0c --- /dev/null +++ b/generators/generators/app/templates/core/src/main/java/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for the core-bot sample. + */ +package <%= packageName %>; diff --git a/generators/generators/app/templates/core/src/test/java/ApplicationTest.java b/generators/generators/app/templates/core/src/test/java/ApplicationTest.java new file mode 100644 index 000000000..9b84031d4 --- /dev/null +++ b/generators/generators/app/templates/core/src/test/java/ApplicationTest.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTest { + + @Test + public void contextLoads() { + } + +} diff --git a/generators/generators/app/templates/echo/project/README.md b/generators/generators/app/templates/echo/project/README.md new file mode 100644 index 000000000..0d514610a --- /dev/null +++ b/generators/generators/app/templates/echo/project/README.md @@ -0,0 +1,85 @@ +# <%= botName %> + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +This sample is a Spring Boot app and uses the Azure CLI and azure-webapp Maven plugin to deploy to Azure. + +## Prerequisites + +- Java 1.8+ +- Install [Maven](https://maven.apache.org/) +- An account on [Azure](https://azure.microsoft.com) if you want to deploy to Azure. + +## To try this sample locally +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\<%= artifact %>-1.0.0.jar` + +- Test the bot using Bot Framework Emulator + + [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + + - Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + + - Connect to the bot using Bot Framework Emulator + + - Launch Bot Framework Emulator + - File -> Open Bot + - Enter a Bot URL of `http://localhost:3978/api/messages` + +## Deploy the bot to Azure + +As described on [Deploy your bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-deploy-az-cli), you will perform the first 4 steps to setup the Azure app, then deploy the code using the azure-webapp Maven plugin. + +### 1. Login to Azure +From a command (or PowerShell) prompt in the root of the bot folder, execute: +`az login` + +### 2. Set the subscription +`az account set --subscription ""` + +If you aren't sure which subscription to use for deploying the bot, you can view the list of subscriptions for your account by using `az account list` command. + +### 3. Create an App registration +`az ad app create --display-name "" --password "" --available-to-other-tenants` + +Replace `` and `` with your own values. + +`` is the unique name of your bot. +`` is a minimum 16 character password for your bot. + +Record the `appid` from the returned JSON + +### 4. Create the Azure resources +Replace the values for ``, ``, ``, and `` in the following commands: + +#### To a new Resource Group +`az deployment sub create --name "echoBotDeploy" --location "westus" --template-file ".\deploymentTemplates\template-with-new-rg.json" --parameters appId="" appSecret="" botId="" botSku=S1 newAppServicePlanName="echoBotPlan" newWebAppName="echoBot" groupLocation="westus" newAppServicePlanLocation="westus"` + +#### To an existing Resource Group +`az deployment group create --resource-group "" --template-file ".\deploymentTemplates\template-with-preexisting-rg.json" --parameters appId="" appSecret="" botId="" newWebAppName="echoBot" newAppServicePlanName="echoBotPlan" appServicePlanLocation="westus" --name "echoBot"` + +### 5. Update app id and password +In src/main/resources/application.properties update +- `MicrosoftAppPassword` with the botsecret value +- `MicrosoftAppId` with the appid from the first step + +### 6. Deploy the code +- Execute `mvn clean package` +- Execute `mvn azure-webapp:deploy -Dgroupname="" -Dbotname=""` + +If the deployment is successful, you will be able to test it via "Test in Web Chat" from the Azure Portal using the "Bot Channel Registration" for the bot. + +After the bot is deployed, you only need to execute #6 if you make changes to the bot. + + +## Further reading + +- [Maven Plugin for Azure App Service](https://docs.microsoft.com/en-us/java/api/overview/azure/maven/azure-webapp-maven-plugin/readme?view=azure-java-stable) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [Azure for Java cloud developers](https://docs.microsoft.com/en-us/azure/java/?view=azure-java-stable) +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) diff --git a/generators/generators/app/templates/echo/project/deploymentTemplates/template-with-new-rg.json b/generators/generators/app/templates/echo/project/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..196cfb933 --- /dev/null +++ b/generators/generators/app/templates/echo/project/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,292 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "azurebot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} diff --git a/generators/generators/app/templates/echo/project/deploymentTemplates/template-with-preexisting-rg.json b/generators/generators/app/templates/echo/project/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..d6feb0a0f --- /dev/null +++ b/generators/generators/app/templates/echo/project/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,260 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "azurebot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} diff --git a/generators/generators/app/templates/echo/project/pom.xml b/generators/generators/app/templates/echo/project/pom.xml new file mode 100644 index 000000000..2e9fe1986 --- /dev/null +++ b/generators/generators/app/templates/echo/project/pom.xml @@ -0,0 +1,259 @@ + + + + 4.0.0 + + <%= packageName %> + <%= artifact %> + 1.0.0 + jar + + ${project.groupId}:${project.artifactId} + This package contains a Java Bot Echo. + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 2.4.0 + + + + + 1.8 + 1.8 + 1.8 + <%= packageName %>.Application + https://botbuilder.myget.org/F/botbuilder-v4-java-daily/maven/ + + + + + junit + junit + 4.13.1 + test + + + org.springframework.boot + spring-boot-starter-test + 2.4.0 + test + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + org.apache.logging.log4j + log4j-core + 2.17.1 + + + org.apache.logging.log4j + log4j-to-slf4j + 2.15.0 + test + + + + com.microsoft.bot + bot-integration-spring + 4.14.1 + compile + + + + + + MyGet + ${repo.url} + + + + + + ossrh + + https://oss.sonatype.org/ + + + + + + + build + + true + + + + + src/main/resources + false + + + + + + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + <%= packageName %>.Application + + + + + + com.microsoft.azure + azure-webapp-maven-plugin + 1.7.0 + + V2 + ${groupname} + ${botname} + + + JAVA_OPTS + -Dserver.port=80 + + + + linux + jre8 + jre8 + + + + + ${project.basedir}/target + + *.jar + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.7.1 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.0.0 + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + true + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + false + + + + attach-javadocs + + jar + + + + + + + + + diff --git a/generators/generators/app/templates/echo/project/src/main/resources/application.properties b/generators/generators/app/templates/echo/project/src/main/resources/application.properties new file mode 100644 index 000000000..d7d0ee864 --- /dev/null +++ b/generators/generators/app/templates/echo/project/src/main/resources/application.properties @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +server.port=3978 diff --git a/generators/generators/app/templates/echo/project/src/main/resources/log4j2.json b/generators/generators/app/templates/echo/project/src/main/resources/log4j2.json new file mode 100644 index 000000000..ad838e77f --- /dev/null +++ b/generators/generators/app/templates/echo/project/src/main/resources/log4j2.json @@ -0,0 +1,21 @@ +{ + "configuration": { + "name": "Default", + "appenders": { + "Console": { + "name": "Console-Appender", + "target": "SYSTEM_OUT", + "PatternLayout": { + "pattern": + "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" + } + } + }, + "loggers": { + "root": { + "level": "debug", + "appender-ref": { "ref": "Console-Appender", "level": "debug" } + } + } + } +} diff --git a/generators/generators/app/templates/echo/project/src/main/webapp/META-INF/MANIFEST.MF b/generators/generators/app/templates/echo/project/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/generators/generators/app/templates/echo/project/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/generators/generators/app/templates/echo/project/src/main/webapp/WEB-INF/web.xml b/generators/generators/app/templates/echo/project/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/generators/generators/app/templates/echo/project/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + dispatcher + + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + /WEB-INF/spring/dispatcher-config.xml + + 1 + \ No newline at end of file diff --git a/generators/generators/app/templates/echo/project/src/main/webapp/index.html b/generators/generators/app/templates/echo/project/src/main/webapp/index.html new file mode 100644 index 000000000..1b10a9051 --- /dev/null +++ b/generators/generators/app/templates/echo/project/src/main/webapp/index.html @@ -0,0 +1,418 @@ + + + + + + + <%= botName %> + + + + + +
+
+
+
<%= botName %>
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + + diff --git a/generators/generators/app/templates/echo/src/main/java/Application.java b/generators/generators/app/templates/echo/src/main/java/Application.java new file mode 100644 index 000000000..001ead194 --- /dev/null +++ b/generators/generators/app/templates/echo/src/main/java/Application.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.integration.AdapterWithErrorHandler; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.integration.spring.BotController; +import com.microsoft.bot.integration.spring.BotDependencyConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +// +// This is the starting point of the Sprint Boot Bot application. +// +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// org.springframework.web.bind.annotation.RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +/** + * This class extends the BotDependencyConfiguration which provides the default + * implementations for a Bot application. The Application class should + * override methods in order to provide custom implementations. + */ +public class Application extends BotDependencyConfiguration { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * Returns the Bot for this application. + * + *

+ * The @Component annotation could be used on the Bot class instead of this method + * with the @Bean annotation. + *

+ * + * @return The Bot implementation for this application. + */ + @Bean + public Bot getBot() { + return new EchoBot(); + } + + /** + * Returns a custom Adapter that provides error handling. + * + * @param configuration The Configuration object to use. + * @return An error handling BotFrameworkHttpAdapter. + */ + @Override + public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) { + return new AdapterWithErrorHandler(configuration); + } +} diff --git a/generators/generators/app/templates/echo/src/main/java/EchoBot.java b/generators/generators/app/templates/echo/src/main/java/EchoBot.java new file mode 100644 index 000000000..9a474ab44 --- /dev/null +++ b/generators/generators/app/templates/echo/src/main/java/EchoBot.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.schema.ChannelAccount; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * This class implements the functionality of the Bot. + * + *

+ * This is where application specific logic for interacting with the users would be added. For this + * sample, the {@link #onMessageActivity(TurnContext)} echos the text back to the user. The {@link + * #onMembersAdded(List, TurnContext)} will send a greeting to new conversation participants. + *

+ */ +public class EchoBot extends ActivityHandler { + + @Override + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + return turnContext.sendActivity( + MessageFactory.text("Echo: " + turnContext.getActivity().getText()) + ).thenApply(sendResult -> null); + } + + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, + TurnContext turnContext + ) { + return membersAdded.stream() + .filter( + member -> !StringUtils + .equals(member.getId(), turnContext.getActivity().getRecipient().getId()) + ).map(channel -> turnContext.sendActivity(MessageFactory.text("Hello and welcome!"))) + .collect(CompletableFutures.toFutureList()).thenApply(resourceResponses -> null); + } +} diff --git a/generators/generators/app/templates/echo/src/test/java/ApplicationTests.java b/generators/generators/app/templates/echo/src/test/java/ApplicationTests.java new file mode 100644 index 000000000..37084390c --- /dev/null +++ b/generators/generators/app/templates/echo/src/test/java/ApplicationTests.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/generators/generators/app/templates/empty/project/README.md b/generators/generators/app/templates/empty/project/README.md new file mode 100644 index 000000000..0d514610a --- /dev/null +++ b/generators/generators/app/templates/empty/project/README.md @@ -0,0 +1,85 @@ +# <%= botName %> + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +This sample is a Spring Boot app and uses the Azure CLI and azure-webapp Maven plugin to deploy to Azure. + +## Prerequisites + +- Java 1.8+ +- Install [Maven](https://maven.apache.org/) +- An account on [Azure](https://azure.microsoft.com) if you want to deploy to Azure. + +## To try this sample locally +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\<%= artifact %>-1.0.0.jar` + +- Test the bot using Bot Framework Emulator + + [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + + - Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + + - Connect to the bot using Bot Framework Emulator + + - Launch Bot Framework Emulator + - File -> Open Bot + - Enter a Bot URL of `http://localhost:3978/api/messages` + +## Deploy the bot to Azure + +As described on [Deploy your bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-deploy-az-cli), you will perform the first 4 steps to setup the Azure app, then deploy the code using the azure-webapp Maven plugin. + +### 1. Login to Azure +From a command (or PowerShell) prompt in the root of the bot folder, execute: +`az login` + +### 2. Set the subscription +`az account set --subscription ""` + +If you aren't sure which subscription to use for deploying the bot, you can view the list of subscriptions for your account by using `az account list` command. + +### 3. Create an App registration +`az ad app create --display-name "" --password "" --available-to-other-tenants` + +Replace `` and `` with your own values. + +`` is the unique name of your bot. +`` is a minimum 16 character password for your bot. + +Record the `appid` from the returned JSON + +### 4. Create the Azure resources +Replace the values for ``, ``, ``, and `` in the following commands: + +#### To a new Resource Group +`az deployment sub create --name "echoBotDeploy" --location "westus" --template-file ".\deploymentTemplates\template-with-new-rg.json" --parameters appId="" appSecret="" botId="" botSku=S1 newAppServicePlanName="echoBotPlan" newWebAppName="echoBot" groupLocation="westus" newAppServicePlanLocation="westus"` + +#### To an existing Resource Group +`az deployment group create --resource-group "" --template-file ".\deploymentTemplates\template-with-preexisting-rg.json" --parameters appId="" appSecret="" botId="" newWebAppName="echoBot" newAppServicePlanName="echoBotPlan" appServicePlanLocation="westus" --name "echoBot"` + +### 5. Update app id and password +In src/main/resources/application.properties update +- `MicrosoftAppPassword` with the botsecret value +- `MicrosoftAppId` with the appid from the first step + +### 6. Deploy the code +- Execute `mvn clean package` +- Execute `mvn azure-webapp:deploy -Dgroupname="" -Dbotname=""` + +If the deployment is successful, you will be able to test it via "Test in Web Chat" from the Azure Portal using the "Bot Channel Registration" for the bot. + +After the bot is deployed, you only need to execute #6 if you make changes to the bot. + + +## Further reading + +- [Maven Plugin for Azure App Service](https://docs.microsoft.com/en-us/java/api/overview/azure/maven/azure-webapp-maven-plugin/readme?view=azure-java-stable) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [Azure for Java cloud developers](https://docs.microsoft.com/en-us/azure/java/?view=azure-java-stable) +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) diff --git a/generators/generators/app/templates/empty/project/deploymentTemplates/template-with-new-rg.json b/generators/generators/app/templates/empty/project/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..196cfb933 --- /dev/null +++ b/generators/generators/app/templates/empty/project/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,292 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "azurebot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} diff --git a/generators/generators/app/templates/empty/project/deploymentTemplates/template-with-preexisting-rg.json b/generators/generators/app/templates/empty/project/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..d6feb0a0f --- /dev/null +++ b/generators/generators/app/templates/empty/project/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,260 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "azurebot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} diff --git a/generators/generators/app/templates/empty/project/pom.xml b/generators/generators/app/templates/empty/project/pom.xml new file mode 100644 index 000000000..2e9fe1986 --- /dev/null +++ b/generators/generators/app/templates/empty/project/pom.xml @@ -0,0 +1,259 @@ + + + + 4.0.0 + + <%= packageName %> + <%= artifact %> + 1.0.0 + jar + + ${project.groupId}:${project.artifactId} + This package contains a Java Bot Echo. + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 2.4.0 + + + + + 1.8 + 1.8 + 1.8 + <%= packageName %>.Application + https://botbuilder.myget.org/F/botbuilder-v4-java-daily/maven/ + + + + + junit + junit + 4.13.1 + test + + + org.springframework.boot + spring-boot-starter-test + 2.4.0 + test + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + org.apache.logging.log4j + log4j-core + 2.17.1 + + + org.apache.logging.log4j + log4j-to-slf4j + 2.15.0 + test + + + + com.microsoft.bot + bot-integration-spring + 4.14.1 + compile + + + + + + MyGet + ${repo.url} + + + + + + ossrh + + https://oss.sonatype.org/ + + + + + + + build + + true + + + + + src/main/resources + false + + + + + + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + <%= packageName %>.Application + + + + + + com.microsoft.azure + azure-webapp-maven-plugin + 1.7.0 + + V2 + ${groupname} + ${botname} + + + JAVA_OPTS + -Dserver.port=80 + + + + linux + jre8 + jre8 + + + + + ${project.basedir}/target + + *.jar + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.7.1 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.0.0 + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + true + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + false + + + + attach-javadocs + + jar + + + + + + + + + diff --git a/generators/generators/app/templates/empty/project/src/main/resources/application.properties b/generators/generators/app/templates/empty/project/src/main/resources/application.properties new file mode 100644 index 000000000..d7d0ee864 --- /dev/null +++ b/generators/generators/app/templates/empty/project/src/main/resources/application.properties @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +server.port=3978 diff --git a/generators/generators/app/templates/empty/project/src/main/resources/log4j2.json b/generators/generators/app/templates/empty/project/src/main/resources/log4j2.json new file mode 100644 index 000000000..ad838e77f --- /dev/null +++ b/generators/generators/app/templates/empty/project/src/main/resources/log4j2.json @@ -0,0 +1,21 @@ +{ + "configuration": { + "name": "Default", + "appenders": { + "Console": { + "name": "Console-Appender", + "target": "SYSTEM_OUT", + "PatternLayout": { + "pattern": + "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" + } + } + }, + "loggers": { + "root": { + "level": "debug", + "appender-ref": { "ref": "Console-Appender", "level": "debug" } + } + } + } +} diff --git a/generators/generators/app/templates/empty/project/src/main/webapp/META-INF/MANIFEST.MF b/generators/generators/app/templates/empty/project/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/generators/generators/app/templates/empty/project/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/generators/generators/app/templates/empty/project/src/main/webapp/WEB-INF/web.xml b/generators/generators/app/templates/empty/project/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/generators/generators/app/templates/empty/project/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + dispatcher + + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + /WEB-INF/spring/dispatcher-config.xml + + 1 + \ No newline at end of file diff --git a/generators/generators/app/templates/empty/project/src/main/webapp/index.html b/generators/generators/app/templates/empty/project/src/main/webapp/index.html new file mode 100644 index 000000000..1b10a9051 --- /dev/null +++ b/generators/generators/app/templates/empty/project/src/main/webapp/index.html @@ -0,0 +1,418 @@ + + + + + + + <%= botName %> + + + + + +
+
+
+
<%= botName %>
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + + diff --git a/generators/generators/app/templates/empty/src/main/java/Application.java b/generators/generators/app/templates/empty/src/main/java/Application.java new file mode 100644 index 000000000..44992727f --- /dev/null +++ b/generators/generators/app/templates/empty/src/main/java/Application.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.integration.AdapterWithErrorHandler; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.integration.spring.BotController; +import com.microsoft.bot.integration.spring.BotDependencyConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +// +// This is the starting point of the Sprint Boot Bot application. +// +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// org.springframework.web.bind.annotation.RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +/** + * This class extends the BotDependencyConfiguration which provides the default + * implementations for a Bot application. The Application class should + * override methods in order to provide custom implementations. + */ +public class Application extends BotDependencyConfiguration { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * Returns the Bot for this application. + * + *

+ * The @Component annotation could be used on the Bot class instead of this method + * with the @Bean annotation. + *

+ * + * @return The Bot implementation for this application. + */ + @Bean + public Bot getBot() { + return new EmptyBot(); + } + + /** + * Returns a custom Adapter that provides error handling. + * + * @param configuration The Configuration object to use. + * @return An error handling BotFrameworkHttpAdapter. + */ + @Override + public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) { + return new AdapterWithErrorHandler(configuration); + } +} diff --git a/generators/generators/app/templates/empty/src/main/java/EmptyBot.java b/generators/generators/app/templates/empty/src/main/java/EmptyBot.java new file mode 100644 index 000000000..3f8d6208d --- /dev/null +++ b/generators/generators/app/templates/empty/src/main/java/EmptyBot.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.schema.ChannelAccount; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * This class implements the functionality of the Bot. + * + *

+ * This is where application specific logic for interacting with the users would be added. For this + * sample, the {@link #onMessageActivity(TurnContext)} echos the text back to the user. The {@link + * #onMembersAdded(List, TurnContext)} will send a greeting to new conversation participants. + *

+ */ +public class EmptyBot extends ActivityHandler { + + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, + TurnContext turnContext + ) { + return membersAdded.stream() + .filter( + member -> !StringUtils + .equals(member.getId(), turnContext.getActivity().getRecipient().getId()) + ).map(channel -> turnContext.sendActivity(MessageFactory.text("Hello world!"))) + .collect(CompletableFutures.toFutureList()).thenApply(resourceResponses -> null); + } +} diff --git a/generators/generators/app/templates/empty/src/test/java/ApplicationTests.java b/generators/generators/app/templates/empty/src/test/java/ApplicationTests.java new file mode 100644 index 000000000..37084390c --- /dev/null +++ b/generators/generators/app/templates/empty/src/test/java/ApplicationTests.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package <%= packageName %>; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/generators/package-lock.json b/generators/package-lock.json new file mode 100644 index 000000000..1ac056e7d --- /dev/null +++ b/generators/package-lock.json @@ -0,0 +1,1927 @@ +{ + "name": "generator-botbuilder-java", + "version": "4.14.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "generator-botbuilder-java", + "version": "4.14.0", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "chalk": "~4.0.0", + "lodash": "~4.17.15", + "mkdirp": "^1.0.4", + "yeoman-generator": "^5.7.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "dependencies": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-username": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/github-username/-/github-username-6.0.0.tgz", + "integrity": "sha512-7TTrRjxblSI5l6adk9zd+cV5d6i1OrJSo3Vr9xdGqFLBQo0mz5P9eIfKCDJ7eekVGGFLbce0qbPSnktXV2BjDQ==", + "dependencies": { + "@octokit/rest": "^18.0.6" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/sort-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", + "integrity": "sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==", + "dependencies": { + "is-plain-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yeoman-generator": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-5.7.0.tgz", + "integrity": "sha512-z9ZwgKoDOd+llPDCwn8Ax2l4In5FMhlslxdeByW4AMxhT+HbTExXKEAahsClHSbwZz1i5OzRwLwRIUdOJBr5Bw==", + "dependencies": { + "chalk": "^4.1.0", + "dargs": "^7.0.0", + "debug": "^4.1.1", + "execa": "^5.1.1", + "github-username": "^6.0.0", + "lodash": "^4.17.11", + "minimist": "^1.2.5", + "read-pkg-up": "^7.0.1", + "run-async": "^2.0.0", + "semver": "^7.2.1", + "shelljs": "^0.8.5", + "sort-keys": "^4.2.0", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=12.10.0" + }, + "peerDependencies": { + "yeoman-environment": "^3.2.0" + }, + "peerDependenciesMeta": { + "yeoman-environment": { + "optional": true + } + } + }, + "node_modules/yeoman-generator/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "requires": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "requires": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "requires": { + "@octokit/types": "^6.40.0" + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "requires": {} + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "requires": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + } + }, + "@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "requires": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "requires": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "requires": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "github-username": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/github-username/-/github-username-6.0.0.tgz", + "integrity": "sha512-7TTrRjxblSI5l6adk9zd+cV5d6i1OrJSo3Vr9xdGqFLBQo0mz5P9eIfKCDJ7eekVGGFLbce0qbPSnktXV2BjDQ==", + "requires": { + "@octokit/rest": "^18.0.6" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "requires": { + "has": "^1.0.3" + } + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "requires": { + "resolve": "^1.1.6" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "sort-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", + "integrity": "sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==", + "requires": { + "is-plain-obj": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yeoman-generator": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-5.7.0.tgz", + "integrity": "sha512-z9ZwgKoDOd+llPDCwn8Ax2l4In5FMhlslxdeByW4AMxhT+HbTExXKEAahsClHSbwZz1i5OzRwLwRIUdOJBr5Bw==", + "requires": { + "chalk": "^4.1.0", + "dargs": "^7.0.0", + "debug": "^4.1.1", + "execa": "^5.1.1", + "github-username": "^6.0.0", + "lodash": "^4.17.11", + "minimist": "^1.2.5", + "read-pkg-up": "^7.0.1", + "run-async": "^2.0.0", + "semver": "^7.2.1", + "shelljs": "^0.8.5", + "sort-keys": "^4.2.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + } + } +} diff --git a/generators/package.json b/generators/package.json new file mode 100644 index 000000000..6c582eac3 --- /dev/null +++ b/generators/package.json @@ -0,0 +1,32 @@ +{ + "name": "generator-botbuilder-java", + "version": "4.14.0", + "description": "A yeoman generator for creating Java bots built with Bot Framework v4", + "homepage": "https://github.com/Microsoft/BotBuilder-Java/tree/main/generators", + "author": { + "name": "Microsoft", + "email": "botframework@microsoft.com", + "url": "http://dev.botframework.com" + }, + "license": "SEE LICENSE IN LICENSE.md", + "repository": "https://github.com/Microsoft/BotBuilder-Java.git", + "files": [ + "components", + "generators" + ], + "keywords": [ + "botbuilder", + "bots", + "bot framework", + "yeoman-generator", + "Microsoft AI", + "Microsoft Teams", + "Conversational AI" + ], + "dependencies": { + "chalk": "~4.0.0", + "lodash": "~4.17.15", + "mkdirp": "^1.0.4", + "yeoman-generator": "^5.7.0" + } +} diff --git a/generators/target/npmlist.json b/generators/target/npmlist.json new file mode 100644 index 000000000..433027994 --- /dev/null +++ b/generators/target/npmlist.json @@ -0,0 +1 @@ +{"name":"generator-java","version":"4.9.1","dependencies":{"camelcase":{"version":"6.0.0"},"chalk":{"version":"4.0.0","dependencies":{"ansi-styles":{"version":"4.3.0","dependencies":{"color-convert":{"version":"2.0.1","dependencies":{"color-name":{"version":"1.1.4"}}}}},"supports-color":{"version":"7.2.0","dependencies":{"has-flag":{"version":"4.0.0"}}}}},"lodash":{"version":"4.17.20"},"mkdirp":{"version":"1.0.4"},"yeoman-generator":{"version":"4.10.1","dependencies":{"async":{"version":"2.6.3","dependencies":{"lodash":{"version":"4.17.20"}}},"cli-table":{"version":"0.3.4","dependencies":{"chalk":{"version":"2.4.2","dependencies":{"ansi-styles":{"version":"3.2.1","dependencies":{"color-convert":{"version":"1.9.3","dependencies":{"color-name":{"version":"1.1.3"}}}}},"supports-color":{"version":"5.5.0","dependencies":{"has-flag":{"version":"3.0.0"}}},"escape-string-regexp":{"version":"1.0.5"}}},"string-width":{"version":"4.2.0","dependencies":{"emoji-regex":{"version":"8.0.0"},"is-fullwidth-code-point":{"version":"3.0.0"},"strip-ansi":{"version":"6.0.0","dependencies":{"ansi-regex":{"version":"5.0.0"}}}}}}},"cross-spawn":{"version":"6.0.5","dependencies":{"semver":{"version":"5.7.1"},"nice-try":{"version":"1.0.5"},"path-key":{"version":"2.0.1"},"shebang-command":{"version":"1.2.0","dependencies":{"shebang-regex":{"version":"1.0.0"}}},"which":{"version":"1.3.1","dependencies":{"isexe":{"version":"2.0.0"}}}}},"dargs":{"version":"6.1.0"},"dateformat":{"version":"3.0.3"},"debug":{"version":"4.3.1","dependencies":{"ms":{"version":"2.1.2"}}},"diff":{"version":"4.0.2"},"error":{"version":"7.2.1","dependencies":{"string-template":{"version":"0.2.1"}}},"find-up":{"version":"3.0.0","dependencies":{"locate-path":{"version":"3.0.0","dependencies":{"p-locate":{"version":"3.0.0","dependencies":{"p-limit":{"version":"2.3.0","dependencies":{"p-try":{"version":"2.2.0"}}}}},"path-exists":{"version":"3.0.0"}}}}},"github-username":{"version":"3.0.0","dependencies":{"gh-got":{"version":"5.0.0","dependencies":{"got":{"version":"6.7.1","dependencies":{"create-error-class":{"version":"3.0.2","dependencies":{"capture-stack-trace":{"version":"1.0.1"}}},"duplexer3":{"version":"0.1.4"},"get-stream":{"version":"3.0.0"},"is-redirect":{"version":"1.0.0"},"is-retry-allowed":{"version":"1.2.0"},"is-stream":{"version":"1.1.0"},"lowercase-keys":{"version":"1.0.1"},"safe-buffer":{"version":"5.2.1"},"timed-out":{"version":"4.0.1"},"unzip-response":{"version":"2.0.1"},"url-parse-lax":{"version":"1.0.0","dependencies":{"prepend-http":{"version":"1.0.4"}}}}},"is-plain-obj":{"version":"1.1.0"}}}}},"grouped-queue":{"version":"1.1.0","dependencies":{"lodash":{"version":"4.17.20"}}},"istextorbinary":{"version":"2.6.0","dependencies":{"binaryextensions":{"version":"2.3.0"},"editions":{"version":"2.3.1","dependencies":{"semver":{"version":"6.3.0"},"errlop":{"version":"2.2.0"}}},"textextensions":{"version":"2.6.0"}}},"lodash":{"version":"4.17.20"},"make-dir":{"version":"3.1.0","dependencies":{"semver":{"version":"6.3.0"}}},"mem-fs-editor":{"version":"6.0.0","dependencies":{"commondir":{"version":"1.0.1"},"deep-extend":{"version":"0.6.0"},"ejs":{"version":"2.7.4"},"glob":{"version":"7.1.6","dependencies":{"fs.realpath":{"version":"1.0.0"},"inflight":{"version":"1.0.6","dependencies":{"once":{"version":"1.4.0"},"wrappy":{"version":"1.0.2"}}},"inherits":{"version":"2.0.4"},"minimatch":{"version":"3.0.4","dependencies":{"brace-expansion":{"version":"1.1.11","dependencies":{"balanced-match":{"version":"1.0.0"},"concat-map":{"version":"0.0.1"}}}}},"once":{"version":"1.4.0","dependencies":{"wrappy":{"version":"1.0.2"}}},"path-is-absolute":{"version":"1.0.1"}}},"globby":{"version":"9.2.0","dependencies":{"@types/glob":{"version":"7.1.3","dependencies":{"@types/minimatch":{"version":"3.0.3"},"@types/node":{"version":"14.14.11"}}},"array-union":{"version":"1.0.2","dependencies":{"array-uniq":{"version":"1.0.3"}}},"dir-glob":{"version":"2.2.2","dependencies":{"path-type":{"version":"3.0.0","dependencies":{"pify":{"version":"3.0.0"}}}}},"fast-glob":{"version":"2.2.7","dependencies":{"@mrmlnc/readdir-enhanced":{"version":"2.2.1","dependencies":{"call-me-maybe":{"version":"1.0.1"},"glob-to-regexp":{"version":"0.3.0"}}},"@nodelib/fs.stat":{"version":"1.1.3"},"glob-parent":{"version":"3.1.0","dependencies":{"is-glob":{"version":"3.1.0","dependencies":{"is-extglob":{"version":"2.1.1"}}},"path-dirname":{"version":"1.0.2"}}},"is-glob":{"version":"4.0.1","dependencies":{"is-extglob":{"version":"2.1.1"}}},"merge2":{"version":"1.4.1"},"micromatch":{"version":"3.1.10","dependencies":{"arr-diff":{"version":"4.0.0"},"array-unique":{"version":"0.3.2"},"braces":{"version":"2.3.2","dependencies":{"arr-flatten":{"version":"1.1.0"},"array-unique":{"version":"0.3.2"},"extend-shallow":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"}}},"fill-range":{"version":"4.0.0","dependencies":{"extend-shallow":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"}}},"is-number":{"version":"3.0.0","dependencies":{"kind-of":{"version":"3.2.2","dependencies":{"is-buffer":{"version":"1.1.6"}}}}},"repeat-string":{"version":"1.6.1"},"to-regex-range":{"version":"2.1.1","dependencies":{"is-number":{"version":"3.0.0"},"repeat-string":{"version":"1.6.1"}}}}},"isobject":{"version":"3.0.1"},"repeat-element":{"version":"1.1.3"},"snapdragon":{"version":"0.8.2"},"snapdragon-node":{"version":"2.1.1","dependencies":{"isobject":{"version":"3.0.1"},"define-property":{"version":"1.0.0","dependencies":{"is-descriptor":{"version":"1.0.2","dependencies":{"kind-of":{"version":"6.0.3"},"is-accessor-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"is-data-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}}}}}},"snapdragon-util":{"version":"3.0.1","dependencies":{"kind-of":{"version":"3.2.2","dependencies":{"is-buffer":{"version":"1.1.6"}}}}}}},"split-string":{"version":"3.1.0","dependencies":{"extend-shallow":{"version":"3.0.2"}}},"to-regex":{"version":"3.0.2"}}},"define-property":{"version":"2.0.2","dependencies":{"is-descriptor":{"version":"1.0.2","dependencies":{"is-accessor-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"is-data-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"kind-of":{"version":"6.0.3"}}},"isobject":{"version":"3.0.1"}}},"extend-shallow":{"version":"3.0.2","dependencies":{"assign-symbols":{"version":"1.0.0"},"is-extendable":{"version":"1.0.1","dependencies":{"is-plain-object":{"version":"2.0.4"}}}}},"extglob":{"version":"2.0.4","dependencies":{"array-unique":{"version":"0.3.2"},"expand-brackets":{"version":"2.1.4","dependencies":{"debug":{"version":"2.6.9","dependencies":{"ms":{"version":"2.0.0"}}},"define-property":{"version":"0.2.5","dependencies":{"is-descriptor":{"version":"0.1.6"}}},"extend-shallow":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"}}},"posix-character-classes":{"version":"0.1.1"},"regex-not":{"version":"1.0.2"},"snapdragon":{"version":"0.8.2"},"to-regex":{"version":"3.0.2"}}},"define-property":{"version":"1.0.0","dependencies":{"is-descriptor":{"version":"1.0.2","dependencies":{"is-accessor-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"is-data-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"kind-of":{"version":"6.0.3"}}}}},"extend-shallow":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"}}},"fragment-cache":{"version":"0.2.1"},"regex-not":{"version":"1.0.2"},"snapdragon":{"version":"0.8.2"},"to-regex":{"version":"3.0.2"}}},"fragment-cache":{"version":"0.2.1","dependencies":{"map-cache":{"version":"0.2.2"}}},"kind-of":{"version":"6.0.3"},"nanomatch":{"version":"1.2.13","dependencies":{"arr-diff":{"version":"4.0.0"},"array-unique":{"version":"0.3.2"},"define-property":{"version":"2.0.2"},"extend-shallow":{"version":"3.0.2"},"fragment-cache":{"version":"0.2.1"},"is-windows":{"version":"1.0.2"},"kind-of":{"version":"6.0.3"},"object.pick":{"version":"1.3.0"},"regex-not":{"version":"1.0.2"},"snapdragon":{"version":"0.8.2"},"to-regex":{"version":"3.0.2"}}},"object.pick":{"version":"1.3.0","dependencies":{"isobject":{"version":"3.0.1"}}},"regex-not":{"version":"1.0.2","dependencies":{"extend-shallow":{"version":"3.0.2"},"safe-regex":{"version":"1.1.0","dependencies":{"ret":{"version":"0.1.15"}}}}},"snapdragon":{"version":"0.8.2","dependencies":{"base":{"version":"0.11.2","dependencies":{"define-property":{"version":"1.0.0","dependencies":{"is-descriptor":{"version":"1.0.2","dependencies":{"is-accessor-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"is-data-descriptor":{"version":"1.0.0","dependencies":{"kind-of":{"version":"6.0.3"}}},"kind-of":{"version":"6.0.3"}}}}},"cache-base":{"version":"1.0.1","dependencies":{"collection-visit":{"version":"1.0.0","dependencies":{"map-visit":{"version":"1.0.0","dependencies":{"object-visit":{"version":"1.0.1"}}},"object-visit":{"version":"1.0.1","dependencies":{"isobject":{"version":"3.0.1"}}}}},"component-emitter":{"version":"1.3.0"},"get-value":{"version":"2.0.6"},"has-value":{"version":"1.0.0","dependencies":{"get-value":{"version":"2.0.6"},"has-values":{"version":"1.0.0","dependencies":{"kind-of":{"version":"4.0.0","dependencies":{"is-buffer":{"version":"1.1.6"}}},"is-number":{"version":"3.0.0"}}},"isobject":{"version":"3.0.1"}}},"isobject":{"version":"3.0.1"},"set-value":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"},"is-plain-object":{"version":"2.0.4"},"extend-shallow":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"}}},"split-string":{"version":"3.1.0"}}},"to-object-path":{"version":"0.3.0"},"union-value":{"version":"1.0.1","dependencies":{"arr-union":{"version":"3.1.0"},"get-value":{"version":"2.0.6"},"is-extendable":{"version":"0.1.1"},"set-value":{"version":"2.0.1"}}},"unset-value":{"version":"1.0.0","dependencies":{"isobject":{"version":"3.0.1"},"has-value":{"version":"0.3.1","dependencies":{"get-value":{"version":"2.0.6"},"isobject":{"version":"2.1.0","dependencies":{"isarray":{"version":"1.0.0"}}},"has-values":{"version":"0.1.4"}}}}}}},"class-utils":{"version":"0.3.6","dependencies":{"arr-union":{"version":"3.1.0"},"define-property":{"version":"0.2.5","dependencies":{"is-descriptor":{"version":"0.1.6"}}},"isobject":{"version":"3.0.1"},"static-extend":{"version":"0.1.2","dependencies":{"object-copy":{"version":"0.1.0","dependencies":{"copy-descriptor":{"version":"0.1.1"},"define-property":{"version":"0.2.5","dependencies":{"is-descriptor":{"version":"0.1.6"}}},"kind-of":{"version":"3.2.2","dependencies":{"is-buffer":{"version":"1.1.6"}}}}},"define-property":{"version":"0.2.5","dependencies":{"is-descriptor":{"version":"0.1.6"}}}}}}},"component-emitter":{"version":"1.3.0"},"isobject":{"version":"3.0.1"},"mixin-deep":{"version":"1.3.2","dependencies":{"for-in":{"version":"1.0.2"},"is-extendable":{"version":"1.0.1","dependencies":{"is-plain-object":{"version":"2.0.4"}}}}},"pascalcase":{"version":"0.1.1"}}},"map-cache":{"version":"0.2.2"},"debug":{"version":"2.6.9","dependencies":{"ms":{"version":"2.0.0"}}},"define-property":{"version":"0.2.5","dependencies":{"is-descriptor":{"version":"0.1.6","dependencies":{"is-accessor-descriptor":{"version":"0.1.6","dependencies":{"kind-of":{"version":"3.2.2","dependencies":{"is-buffer":{"version":"1.1.6"}}}}},"is-data-descriptor":{"version":"0.1.4","dependencies":{"kind-of":{"version":"3.2.2","dependencies":{"is-buffer":{"version":"1.1.6"}}}}},"kind-of":{"version":"5.1.0"}}}}},"extend-shallow":{"version":"2.0.1","dependencies":{"is-extendable":{"version":"0.1.1"}}},"source-map":{"version":"0.5.7"},"source-map-resolve":{"version":"0.5.3","dependencies":{"atob":{"version":"2.1.2"},"decode-uri-component":{"version":"0.2.0"},"resolve-url":{"version":"0.2.1"},"source-map-url":{"version":"0.4.0"},"urix":{"version":"0.1.0"}}},"use":{"version":"3.1.1"}}},"to-regex":{"version":"3.0.2","dependencies":{"define-property":{"version":"2.0.2"},"extend-shallow":{"version":"3.0.2"},"regex-not":{"version":"1.0.2"},"safe-regex":{"version":"1.1.0"}}}}}}},"glob":{"version":"7.1.6"},"ignore":{"version":"4.0.6"},"pify":{"version":"4.0.1"},"slash":{"version":"2.0.0"}}},"isbinaryfile":{"version":"4.0.6"},"mkdirp":{"version":"0.5.5","dependencies":{"minimist":{"version":"1.2.5"}}},"multimatch":{"version":"4.0.0","dependencies":{"@types/minimatch":{"version":"3.0.3"},"array-differ":{"version":"3.0.0"},"arrify":{"version":"2.0.1"},"minimatch":{"version":"3.0.4"},"array-union":{"version":"2.1.0"}}},"rimraf":{"version":"2.7.1"},"through2":{"version":"3.0.2"},"vinyl":{"version":"2.2.1","dependencies":{"clone":{"version":"2.1.2"},"clone-buffer":{"version":"1.0.0"},"clone-stats":{"version":"1.0.0"},"cloneable-readable":{"version":"1.1.3","dependencies":{"readable-stream":{"version":"2.3.7","dependencies":{"safe-buffer":{"version":"5.1.2"},"string_decoder":{"version":"1.1.1","dependencies":{"safe-buffer":{"version":"5.1.2"}}},"core-util-is":{"version":"1.0.2"},"inherits":{"version":"2.0.4"},"isarray":{"version":"1.0.0"},"process-nextick-args":{"version":"2.0.1"},"util-deprecate":{"version":"1.0.2"}}},"inherits":{"version":"2.0.4"},"process-nextick-args":{"version":"2.0.1"}}},"remove-trailing-separator":{"version":"1.1.0"},"replace-ext":{"version":"1.0.1"}}}}},"minimist":{"version":"1.2.5"},"pretty-bytes":{"version":"5.4.1"},"read-chunk":{"version":"3.2.0","dependencies":{"pify":{"version":"4.0.1"},"with-open-file":{"version":"0.1.7","dependencies":{"p-finally":{"version":"1.0.0"},"p-try":{"version":"2.2.0"},"pify":{"version":"4.0.1"}}}}},"read-pkg-up":{"version":"5.0.0","dependencies":{"find-up":{"version":"3.0.0"},"read-pkg":{"version":"5.2.0","dependencies":{"@types/normalize-package-data":{"version":"2.4.0"},"normalize-package-data":{"version":"2.5.0","dependencies":{"hosted-git-info":{"version":"2.8.8"},"semver":{"version":"5.7.1"},"resolve":{"version":"1.19.0"},"validate-npm-package-license":{"version":"3.0.4","dependencies":{"spdx-correct":{"version":"3.1.1","dependencies":{"spdx-expression-parse":{"version":"3.0.1"},"spdx-license-ids":{"version":"3.0.7"}}},"spdx-expression-parse":{"version":"3.0.1","dependencies":{"spdx-exceptions":{"version":"2.3.0"},"spdx-license-ids":{"version":"3.0.7"}}}}}}},"parse-json":{"version":"5.1.0","dependencies":{"@babel/code-frame":{"version":"7.10.4","dependencies":{"@babel/highlight":{"version":"7.10.4","dependencies":{"@babel/helper-validator-identifier":{"version":"7.10.4"},"chalk":{"version":"2.4.2","dependencies":{"ansi-styles":{"version":"3.2.1","dependencies":{"color-convert":{"version":"1.9.3","dependencies":{"color-name":{"version":"1.1.3"}}}}},"supports-color":{"version":"5.5.0","dependencies":{"has-flag":{"version":"3.0.0"}}},"escape-string-regexp":{"version":"1.0.5"}}},"js-tokens":{"version":"4.0.0"}}}}},"error-ex":{"version":"1.3.2","dependencies":{"is-arrayish":{"version":"0.2.1"}}},"json-parse-even-better-errors":{"version":"2.3.1"},"lines-and-columns":{"version":"1.1.6"}}},"type-fest":{"version":"0.6.0"}}}}},"rimraf":{"version":"2.7.1","dependencies":{"glob":{"version":"7.1.6"}}},"run-async":{"version":"2.4.1"},"semver":{"version":"7.3.4","dependencies":{"lru-cache":{"version":"6.0.0","dependencies":{"yallist":{"version":"4.0.0"}}}}},"shelljs":{"version":"0.8.4","dependencies":{"glob":{"version":"7.1.6"},"interpret":{"version":"1.4.0"},"rechoir":{"version":"0.6.2","dependencies":{"resolve":{"version":"1.19.0","dependencies":{"is-core-module":{"version":"2.2.0","dependencies":{"has":{"version":"1.0.3","dependencies":{"function-bind":{"version":"1.1.1"}}}}},"path-parse":{"version":"1.0.6"}}}}}}},"text-table":{"version":"0.2.0"},"through2":{"version":"3.0.2","dependencies":{"inherits":{"version":"2.0.4"},"readable-stream":{"version":"3.6.0","dependencies":{"inherits":{"version":"2.0.4"},"string_decoder":{"version":"1.3.0","dependencies":{"safe-buffer":{"version":"5.2.1"}}},"util-deprecate":{"version":"1.0.2"}}}}},"yeoman-environment":{"version":"2.10.3","dependencies":{"escape-string-regexp":{"version":"1.0.5"},"execa":{"version":"4.1.0","dependencies":{"cross-spawn":{"version":"7.0.3","dependencies":{"path-key":{"version":"3.1.1"},"shebang-command":{"version":"2.0.0","dependencies":{"shebang-regex":{"version":"3.0.0"}}},"which":{"version":"2.0.2","dependencies":{"isexe":{"version":"2.0.0"}}}}},"get-stream":{"version":"5.2.0","dependencies":{"pump":{"version":"3.0.0","dependencies":{"end-of-stream":{"version":"1.4.4","dependencies":{"once":{"version":"1.4.0"}}},"once":{"version":"1.4.0"}}}}},"is-stream":{"version":"2.0.0"},"human-signals":{"version":"1.1.1"},"merge-stream":{"version":"2.0.0"},"npm-run-path":{"version":"4.0.1","dependencies":{"path-key":{"version":"3.1.1"}}},"onetime":{"version":"5.1.2","dependencies":{"mimic-fn":{"version":"2.1.0"}}},"signal-exit":{"version":"3.0.3"},"strip-final-newline":{"version":"2.0.0"}}},"grouped-queue":{"version":"1.1.0"},"inquirer":{"version":"7.3.3","dependencies":{"ansi-escapes":{"version":"4.3.1","dependencies":{"type-fest":{"version":"0.11.0"}}},"cli-cursor":{"version":"3.1.0","dependencies":{"restore-cursor":{"version":"3.1.0","dependencies":{"onetime":{"version":"5.1.2"},"signal-exit":{"version":"3.0.3"}}}}},"cli-width":{"version":"3.0.0"},"external-editor":{"version":"3.1.0","dependencies":{"chardet":{"version":"0.7.0"},"iconv-lite":{"version":"0.4.24","dependencies":{"safer-buffer":{"version":"2.1.2"}}},"tmp":{"version":"0.0.33","dependencies":{"os-tmpdir":{"version":"1.0.2"}}}}},"figures":{"version":"3.2.0","dependencies":{"escape-string-regexp":{"version":"1.0.5"}}},"chalk":{"version":"4.1.0","dependencies":{"ansi-styles":{"version":"4.3.0","dependencies":{"color-convert":{"version":"2.0.1","dependencies":{"color-name":{"version":"1.1.4"}}}}},"supports-color":{"version":"7.2.0","dependencies":{"has-flag":{"version":"4.0.0"}}}}},"lodash":{"version":"4.17.20"},"mute-stream":{"version":"0.0.8"},"run-async":{"version":"2.4.1"},"rxjs":{"version":"6.6.3","dependencies":{"tslib":{"version":"1.14.1"}}},"string-width":{"version":"4.2.0"},"strip-ansi":{"version":"6.0.0"},"through":{"version":"2.3.8"}}},"is-scoped":{"version":"1.0.0","dependencies":{"scoped-regex":{"version":"1.0.0"}}},"lodash":{"version":"4.17.20"},"log-symbols":{"version":"2.2.0","dependencies":{"chalk":{"version":"2.4.2","dependencies":{"escape-string-regexp":{"version":"1.0.5"},"ansi-styles":{"version":"3.2.1","dependencies":{"color-convert":{"version":"1.9.3","dependencies":{"color-name":{"version":"1.1.3"}}}}},"supports-color":{"version":"5.5.0","dependencies":{"has-flag":{"version":"3.0.0"}}}}}}},"mem-fs":{"version":"1.2.0","dependencies":{"through2":{"version":"3.0.2"},"vinyl":{"version":"2.2.1"},"vinyl-file":{"version":"3.0.0","dependencies":{"graceful-fs":{"version":"4.2.4"},"strip-bom-buf":{"version":"1.0.0","dependencies":{"is-utf8":{"version":"0.2.1"}}},"strip-bom-stream":{"version":"2.0.0","dependencies":{"first-chunk-stream":{"version":"2.0.0","dependencies":{"readable-stream":{"version":"2.3.7","dependencies":{"core-util-is":{"version":"1.0.2"},"safe-buffer":{"version":"5.1.2"},"string_decoder":{"version":"1.1.1","dependencies":{"safe-buffer":{"version":"5.1.2"}}},"inherits":{"version":"2.0.4"},"isarray":{"version":"1.0.0"},"process-nextick-args":{"version":"2.0.1"},"util-deprecate":{"version":"1.0.2"}}}}},"strip-bom":{"version":"2.0.0","dependencies":{"is-utf8":{"version":"0.2.1"}}}}},"vinyl":{"version":"2.2.1"},"pify":{"version":"2.3.0"}}}}},"mem-fs-editor":{"version":"6.0.0"},"npm-api":{"version":"1.0.0","dependencies":{"JSONStream":{"version":"1.3.5","dependencies":{"jsonparse":{"version":"1.3.1"},"through":{"version":"2.3.8"}}},"clone-deep":{"version":"4.0.1","dependencies":{"is-plain-object":{"version":"2.0.4","dependencies":{"isobject":{"version":"3.0.1"}}},"kind-of":{"version":"6.0.3"},"shallow-clone":{"version":"3.0.1","dependencies":{"kind-of":{"version":"6.0.3"}}}}},"download-stats":{"version":"0.3.4","dependencies":{"JSONStream":{"version":"1.3.5"},"lazy-cache":{"version":"2.0.2","dependencies":{"set-getter":{"version":"0.1.0","dependencies":{"to-object-path":{"version":"0.3.0","dependencies":{"kind-of":{"version":"3.2.2","dependencies":{"is-buffer":{"version":"1.1.6"}}}}}}}}},"moment":{"version":"2.29.1"}}},"moment":{"version":"2.29.1"},"paged-request":{"version":"2.0.1","dependencies":{"axios":{"version":"0.18.1","dependencies":{"is-buffer":{"version":"2.0.5"},"follow-redirects":{"version":"1.5.10","dependencies":{"debug":{"version":"3.1.0","dependencies":{"ms":{"version":"2.0.0"}}}}}}}}},"request":{"version":"2.88.2","dependencies":{"aws-sign2":{"version":"0.7.0"},"aws4":{"version":"1.11.0"},"caseless":{"version":"0.12.0"},"combined-stream":{"version":"1.0.8","dependencies":{"delayed-stream":{"version":"1.0.0"}}},"extend":{"version":"3.0.2"},"forever-agent":{"version":"0.6.1"},"form-data":{"version":"2.3.3","dependencies":{"asynckit":{"version":"0.4.0"},"combined-stream":{"version":"1.0.8"},"mime-types":{"version":"2.1.27"}}},"har-validator":{"version":"5.1.5","dependencies":{"ajv":{"version":"6.12.6","dependencies":{"fast-deep-equal":{"version":"3.1.3"},"fast-json-stable-stringify":{"version":"2.1.0"},"json-schema-traverse":{"version":"0.4.1"},"uri-js":{"version":"4.4.0","dependencies":{"punycode":{"version":"2.1.1"}}}}},"har-schema":{"version":"2.0.0"}}},"http-signature":{"version":"1.2.0","dependencies":{"assert-plus":{"version":"1.0.0"},"jsprim":{"version":"1.4.1","dependencies":{"assert-plus":{"version":"1.0.0"},"extsprintf":{"version":"1.3.0"},"json-schema":{"version":"0.2.3"},"verror":{"version":"1.10.0","dependencies":{"assert-plus":{"version":"1.0.0"},"core-util-is":{"version":"1.0.2"},"extsprintf":{"version":"1.3.0"}}}}},"sshpk":{"version":"1.16.1","dependencies":{"asn1":{"version":"0.2.4","dependencies":{"safer-buffer":{"version":"2.1.2"}}},"assert-plus":{"version":"1.0.0"},"bcrypt-pbkdf":{"version":"1.0.2","dependencies":{"tweetnacl":{"version":"0.14.5"}}},"dashdash":{"version":"1.14.1","dependencies":{"assert-plus":{"version":"1.0.0"}}},"ecc-jsbn":{"version":"0.1.2","dependencies":{"jsbn":{"version":"0.1.1"},"safer-buffer":{"version":"2.1.2"}}},"getpass":{"version":"0.1.7","dependencies":{"assert-plus":{"version":"1.0.0"}}},"jsbn":{"version":"0.1.1"},"safer-buffer":{"version":"2.1.2"},"tweetnacl":{"version":"0.14.5"}}}}},"is-typedarray":{"version":"1.0.0"},"isstream":{"version":"0.1.2"},"json-stringify-safe":{"version":"5.0.1"},"mime-types":{"version":"2.1.27","dependencies":{"mime-db":{"version":"1.44.0"}}},"oauth-sign":{"version":"0.9.0"},"performance-now":{"version":"2.1.0"},"qs":{"version":"6.5.2"},"uuid":{"version":"3.4.0"},"safe-buffer":{"version":"5.2.1"},"tough-cookie":{"version":"2.5.0","dependencies":{"psl":{"version":"1.8.0"},"punycode":{"version":"2.1.1"}}},"tunnel-agent":{"version":"0.6.0","dependencies":{"safe-buffer":{"version":"5.2.1"}}}}}}},"semver":{"version":"7.3.4"},"text-table":{"version":"0.2.0"},"untildify":{"version":"3.0.3"},"chalk":{"version":"2.4.2","dependencies":{"escape-string-regexp":{"version":"1.0.5"},"ansi-styles":{"version":"3.2.1","dependencies":{"color-convert":{"version":"1.9.3","dependencies":{"color-name":{"version":"1.1.3"}}}}},"supports-color":{"version":"5.5.0","dependencies":{"has-flag":{"version":"3.0.0"}}}}},"debug":{"version":"3.2.7","dependencies":{"ms":{"version":"2.1.2"}}},"diff":{"version":"3.5.0"},"globby":{"version":"8.0.2","dependencies":{"array-union":{"version":"1.0.2"},"fast-glob":{"version":"2.2.7"},"glob":{"version":"7.1.6"},"dir-glob":{"version":"2.0.0","dependencies":{"path-type":{"version":"3.0.0"},"arrify":{"version":"1.0.1"}}},"ignore":{"version":"3.3.10"},"pify":{"version":"3.0.0"},"slash":{"version":"1.0.0"}}},"strip-ansi":{"version":"4.0.0","dependencies":{"ansi-regex":{"version":"3.0.0"}}},"yeoman-generator":{"version":"4.10.1"}}},"chalk":{"version":"2.4.2","dependencies":{"escape-string-regexp":{"version":"1.0.5"},"ansi-styles":{"version":"3.2.1","dependencies":{"color-convert":{"version":"1.9.3","dependencies":{"color-name":{"version":"1.1.3"}}}}},"supports-color":{"version":"5.5.0","dependencies":{"has-flag":{"version":"3.0.0"}}}}}}}}} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/pom.xml b/libraries/bot-ai-luis-v3/pom.xml new file mode 100644 index 000000000..c5d73f696 --- /dev/null +++ b/libraries/bot-ai-luis-v3/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-ai-luis-v3 + jar + + ${project.groupId}:${project.artifactId} + Bot Framework Luis V3 + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-api + + + + com.microsoft.bot + bot-builder + + + com.microsoft.bot + bot-dialogs + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + mockwebserver + test + + + org.json + json + 20190722 + + + + + + build + + true + + + + + + + + diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/DynamicList.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/DynamicList.java new file mode 100644 index 000000000..770ada836 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/DynamicList.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** + * Request Body element to use when passing Dynamic lists to the Luis Service + * call. Defines an extension for a list entity. + * + */ +public class DynamicList { + + /** + * Initializes a new instance of the DynamicList class. + */ + public DynamicList() { + } + + /** + * Initializes a new instance of the DynamicList class. + * + * @param entity Entity field. + * @param requestLists List Elements to use when querying Luis Service. + */ + public DynamicList(String entity, List requestLists) { + this.entity = entity; + this.list = requestLists; + } + + @JsonProperty(value = "listEntityName") + private String entity; + + @JsonProperty(value = "requestLists") + private List list; + + /** + * Gets the name of the list entity to extend. + * + * @return The name of the list entity to extend. + */ + public String getEntity() { + return entity; + } + + /** + * Sets the name of the list entity to extend. + * + * @param entity The name of the list entity to extend. + */ + public void setEntity(String entity) { + this.entity = entity; + } + + /** + * Gets the lists to append on the extended list entity. + * + * @return The lists to append on the extended list entity. + */ + public List getList() { + return list; + } + + /** + * Sets the lists to append on the extended list entity. + * + * @param list The lists to append on the extended list entity. + */ + public void setList(List list) { + this.list = list; + } + + /** + * Validate the object. + * + * @throws IllegalArgumentException on null or invalid values. + */ + public void validate() throws IllegalArgumentException { + // Required: ListEntityName, RequestLists + if (entity == null || list == null) { + throw new IllegalArgumentException("DynamicList requires listEntityName and requestsLists to be defined."); + } + + for (ListElement e : list) { + e.validate(); + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/ExternalEntity.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/ExternalEntity.java new file mode 100644 index 000000000..255824843 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/ExternalEntity.java @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Request Body element to use when passing External Entities to the Luis + * Service call. Defines a user predicted entity that extends an already + * existing one. + * + */ +public class ExternalEntity { + + /** + * Initializes a new instance of ExternalEntity. + */ + public ExternalEntity() { + } + + /** + * Initializes a new instance of ExternalEntity. + * + * @param entity name of the entity to extend. + * @param start start character index of the predicted entity. + * @param length length of the predicted entity. + * @param resolution supplied custom resolution to return as the entity's + * prediction. + */ + public ExternalEntity(String entity, int start, int length, JsonNode resolution) { + this.entity = entity; + this.start = start; + this.length = length; + this.resolution = resolution; + } + + @JsonProperty(value = "entityName") + private String entity; + + @JsonProperty(value = "startIndex") + private int start; + + @JsonProperty(value = "entityLength") + private int length; + + @JsonProperty(value = "resolution") + private JsonNode resolution; + + /** + * Gets the start character index of the predicted entity. + * + * @return The start character index of the predicted entity. + */ + public int getStart() { + return start; + } + + /** + * Sets the start character index of the predicted entity. + * + * @param start The start character index of the predicted entity. + */ + public void setStart(int start) { + this.start = start; + } + + /** + * Gets the name of the entity to extend. + * + * @return The name of the entity to extend. + */ + public String getEntity() { + return entity; + } + + /** + * Sets the name of the entity to extend. + * + * @param entity The name of the entity to extend. + */ + public void setEntity(String entity) { + this.entity = entity; + } + + /** + * Gets the length of the predicted entity. + * + * @return The length of the predicted entity. + */ + public int getLength() { + return length; + } + + /** + * Sets the length of the predicted entity. + * + * @param length The length of the predicted entity. + */ + public void setLength(int length) { + this.length = length; + } + + /** + * Gets a user supplied custom resolution to return as the entity's prediction. + * + * @return A user supplied custom resolution to return as the entity's + * prediction. + */ + public JsonNode getResolution() { + return resolution; + } + + /** + * Sets a user supplied custom resolution to return as the entity's prediction. + * + * @param resolution A user supplied custom resolution to return as the entity's + * prediction. + */ + public void setResolution(JsonNode resolution) { + this.resolution = resolution; + } + + /** + * Validate the object. + * + * @throws IllegalArgumentException on null or invalid values + */ + public void validate() throws IllegalArgumentException { + if (entity == null || length == 0) { + throw new IllegalArgumentException("ExternalEntity requires an EntityName and EntityLength > 0"); + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/ListElement.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/ListElement.java new file mode 100644 index 000000000..edd498ac0 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/ListElement.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * List Element for Dynamic Lists. Defines a sub-list to append to an existing + * list entity. + * + */ +public class ListElement { + + /** + * Initializes a new instance of the ListElement class. + */ + public ListElement() { + } + + /** + * Initializes a new instance of the ListElement class. + * + * @param canonicalForm The canonical form of the sub-list. + * @param synonyms The synonyms of the canonical form. + */ + public ListElement(String canonicalForm, List synonyms) { + this.canonicalForm = canonicalForm; + this.synonyms = synonyms; + } + + /** + * The canonical form of the sub-list. + */ + @JsonProperty(value = "canonicalForm") + private String canonicalForm; + + /** + * The synonyms of the canonical form. + */ + @JsonProperty(value = "synonyms") + @JsonInclude(JsonInclude.Include.NON_NULL) + private List synonyms; + + /** + * Gets the canonical form of the sub-list. + * + * @return String canonical form of the sub-list. + */ + public String getCanonicalForm() { + return canonicalForm; + } + + /** + * Sets the canonical form of the sub-list. + * + * @param canonicalForm the canonical form of the sub-list. + */ + public void setCanonicalForm(String canonicalForm) { + this.canonicalForm = canonicalForm; + } + + /** + * Gets the synonyms of the canonical form. + * + * @return the synonyms List of the canonical form. + */ + public List getSynonyms() { + return synonyms; + } + + /** + * Sets the synonyms of the canonical form. + * + * @param synonyms List of synonyms of the canonical form. + */ + public void setSynonyms(List synonyms) { + this.synonyms = synonyms; + } + + /** + * Validate the object. + * + * @throws IllegalArgumentException if canonicalForm is null. + */ + public void validate() throws IllegalArgumentException { + if (canonicalForm == null) { + throw new IllegalArgumentException("RequestList requires CanonicalForm to be defined."); + } + } + +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisApplication.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisApplication.java new file mode 100644 index 000000000..ad7c575aa --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisApplication.java @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import okhttp3.HttpUrl; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; + +/** + * Luis Application representation with information necessary to query the + * specific Luis Application. Data describing a LUIS application. + * + */ +public class LuisApplication { + + /** + * LUIS application ID. + */ + private String applicationId; + + /** + * LUIS subscription or endpoint key. + */ + private String endpointKey; + + /** + * LUIS endpoint like https://westus.api.cognitive.microsoft.com. + */ + private String endpoint; + + /** + * Initializes a new instance of the Luis Application class. + */ + public LuisApplication() { + } + + /** + * Initializes a new instance of the Luis Application class. + * + * @param applicationId Luis Application ID to query + * @param endpointKey LUIS subscription or endpoint key. + * @param endpoint LUIS endpoint to use like + * https://westus.api.cognitive.microsoft.com + */ + public LuisApplication(String applicationId, String endpointKey, String endpoint) { + setLuisApplication(applicationId, endpointKey, endpoint); + } + + /** + * Initializes a new instance of the Luis Application class. + * + * @param applicationEndpoint LUIS application query endpoint containing + * subscription key and application id as part of the + * url. + */ + public LuisApplication(String applicationEndpoint) { + parse(applicationEndpoint); + } + + /** + * Sets Luis application ID to query. + * + * @param applicationId Luis application ID to query. + */ + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + /** + * Gets LUIS application ID. + * + * @return LUIS application ID. + */ + public String getApplicationId() { + return applicationId; + } + + /** + * Sets the LUIS subscription or endpoint key. + * + * @param endpointKey LUIS subscription or endpoint key. + */ + public void setEndpointKey(String endpointKey) { + this.endpointKey = endpointKey; + } + + /** + * Gets the LUIS subscription or endpoint key. + * + * @return LUIS subscription or endpoint key. + */ + public String getEndpointKey() { + return endpointKey; + } + + /** + * Sets LUIS endpoint like https://westus.api.cognitive.microsoft.com. + * + * @param endpoint LUIS endpoint where application is hosted. + */ + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + /** + * Gets the LUIS endpoint where application is hosted. + * + * @return LUIS endpoint where application is hosted. + */ + public String getEndpoint() { + return endpoint; + } + + /** + * Helper method to set and validate Luis arguments passed. + */ + private void setLuisApplication(String applicationId, String endpointKey, String endpoint) { + + if (!isValidUUID(applicationId)) { + throw new IllegalArgumentException(String.format("%s is not a valid LUIS application id.", applicationId)); + } + + if (!isValidUUID(endpointKey)) { + throw new IllegalArgumentException(String.format("%s is not a valid LUIS subscription key.", endpointKey)); + } + + if (StringUtils.isBlank(endpoint)) { + endpoint = "https://westus.api.cognitive.microsoft.com"; + } + + if (!isValidURL(endpoint)) { + throw new IllegalArgumentException(String.format("%s is not a valid LUIS endpoint.", endpoint)); + } + + this.applicationId = applicationId; + this.endpointKey = endpointKey; + this.endpoint = endpoint; + } + + /** + * Helper method to parse validate and set Luis application members from the + * full application full endpoint. + */ + private void parse(String applicationEndpoint) { + String appId = ""; + try { + String[] segments = new URL(applicationEndpoint).getPath().split("/"); + for (int segment = 0; segment < segments.length - 1; segment++) { + if (segments[segment].equals("apps")) { + appId = segments[segment + 1].trim(); + break; + } + } + } catch (MalformedURLException e) { + throw new IllegalArgumentException( + String.format("Unable to create the LUIS endpoint with the given %s.", applicationEndpoint)); + } + + if (appId.isEmpty()) { + throw new IllegalArgumentException( + String.format("Could not find application Id in %s", applicationEndpoint)); + } + + try { + String endpointKeyParsed = HttpUrl.parse(applicationEndpoint).queryParameterValues("subscription-key") + .stream().findFirst().orElse(""); + + String endpointPared = String.format("%s://%s", new URL(applicationEndpoint).getProtocol(), + new URL(applicationEndpoint).toURI().getHost()); + + setLuisApplication(appId, endpointKeyParsed, endpointPared); + } catch (URISyntaxException | MalformedURLException e) { + throw new IllegalArgumentException( + String.format("Unable to create the LUIS endpoint with the given %s.", applicationEndpoint)); + } + + } + + private boolean isValidUUID(String uuid) { + try { + if (!uuid.contains("-")) { + uuid = uuid.replaceAll("(.{8})(.{4})(.{4})(.{4})(.+)", "$1-$2-$3-$4-$5"); + } + + return UUID.fromString(uuid).toString().equals(uuid); + } catch (IllegalArgumentException e) { + return false; + } + } + + private boolean isValidURL(String uri) { + try { + return new URL(uri).toURI().isAbsolute(); + } catch (URISyntaxException | MalformedURLException exception) { + return false; + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizer.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizer.java new file mode 100644 index 000000000..46b007c2e --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizer.java @@ -0,0 +1,570 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.builder.IntentScore; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.RecognizerConvert; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.schema.Activity; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Luis Recognizer class to query the LUIS Service using the configuration set + * by the LuisRecognizeroptions. A LUIS based implementation of + * TelemetryRecognizer. + */ +public class LuisRecognizer extends TelemetryRecognizer { + /** + * Luis Recognizer options to query the Luis Service. + */ + private LuisRecognizerOptions luisRecognizerOptions; + + /** + * Initializes a new instance of the Luis Recognizer. + * + * @param recognizerOptions Luis Recognizer options to use when calling the LUIS + * Service. + * @throws IllegalArgumentException if null is passed as recognizerOptions. + */ + public LuisRecognizer(LuisRecognizerOptions recognizerOptions) { + if (recognizerOptions == null) { + throw new IllegalArgumentException("Recognizer Options cannot be null"); + } + + this.luisRecognizerOptions = recognizerOptions; + this.setTelemetryClient(recognizerOptions.getTelemetryClient() != null ? recognizerOptions.getTelemetryClient() + : new NullBotTelemetryClient()); + this.setLogPersonalInformation(recognizerOptions.isLogPersonalInformation()); + } + + /** + * Returns the name of the top scoring intent from a set of LUIS results. + * + * @param results The Recognizer Result with the list of Intents to filter. + * Defaults to a value of "None" and a min score value of `0.0` + * @return The top scoring intent name. + */ + public static String topIntent(RecognizerResult results) { + return topIntent(results, "None"); + } + + /** + * Returns the name of the top scoring intent from a set of LUIS results. + * + * @param results The Recognizer Result with the list of Intents to filter + * @param defaultIntent Intent name to return should a top intent be found. + * Defaults to a value of "None" and a min score value of + * `0.0` + * @return The top scoring intent name. + */ + public static String topIntent(RecognizerResult results, String defaultIntent) { + return topIntent(results, defaultIntent, 0.0); + } + + /** + * Returns the name of the top scoring intent from a set of LUIS results. + * + * @param results The Recognizer Result with the list of Intents to filter. + * @param minScore Minimum score needed for an intent to be considered as a top + * intent. + * @return The top scoring intent name. + */ + public static String topIntent(RecognizerResult results, double minScore) { + return topIntent(results, "None", minScore); + } + + /** + * Returns the name of the top scoring intent from a set of LUIS results. + * + * @param results The Recognizer Result with the list of Intents to filter + * @param defaultIntent Intent name to return should a top intent be found. + * Defaults to a value of "None + * @param minScore Minimum score needed for an intent to be considered as a + * top intent. + * @return The top scoring intent name. + */ + public static String topIntent(RecognizerResult results, String defaultIntent, double minScore) { + if (results == null) { + throw new IllegalArgumentException("RecognizerResult"); + } + + defaultIntent = defaultIntent == null || defaultIntent.equals("") ? "None" : defaultIntent; + + String topIntent = null; + double topScore = -1.0; + if (!results.getIntents().isEmpty()) { + for (Map.Entry intent : results.getIntents().entrySet()) { + + double score = intent.getValue().getScore(); + if (score > topScore && score >= minScore) { + topIntent = intent.getKey(); + topScore = score; + } + } + } + + return StringUtils.isNotBlank(topIntent) ? topIntent : defaultIntent; + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param turnContext Context object containing information for a single turn of + * conversation with a user. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + @Override + public CompletableFuture recognize(TurnContext turnContext) { + return recognizeInternal(turnContext, null, null, null); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param dialogContext Context object containing information for a single turn + * of conversation with a user. + * @param activity Activity to recognize. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity) { + return recognizeInternal(dialogContext, activity, null, null, null); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param turnContext Context object containing information for a single turn of + * conversation with a user. + * @param type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(TurnContext turnContext, Class c) { + return recognizeInternal(turnContext, null, null, null) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param dialogContext Context object containing information for a single turn + * of conversation with a user. + * @param activity Activity to recognize. + * @param Type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + Class c) { + return recognizeInternal(dialogContext, activity, null, null, null) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + @Override + public CompletableFuture recognize(TurnContext turnContext, + Map telemetryProperties, Map telemetryMetrics) { + return recognizeInternal(turnContext, null, telemetryProperties, telemetryMetrics); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity Activity to recognize. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + Map telemetryProperties, Map telemetryMetrics) { + return recognizeInternal(dialogContext, activity, null, telemetryProperties, telemetryMetrics); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @param Type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(TurnContext turnContext, + Map telemetryProperties, Map telemetryMetrics, Class c) { + return recognizeInternal(turnContext, null, telemetryProperties, telemetryMetrics) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity Activity to recognize. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @param Type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + Map telemetryProperties, Map telemetryMetrics, Class c) { + return recognizeInternal(dialogContext, activity, null, telemetryProperties, telemetryMetrics) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param recognizerOptions A LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(TurnContext turnContext, + LuisRecognizerOptions recognizerOptions) { + return recognizeInternal(turnContext, recognizerOptions, null, null); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity Activity to recognize. + * @param recognizerOptions A LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + LuisRecognizerOptions recognizerOptions) { + return recognizeInternal(dialogContext, activity, recognizerOptions, null, null); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param recognizerOptions A LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @param type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(TurnContext turnContext, + LuisRecognizerOptions recognizerOptions, Class c) { + return recognizeInternal(turnContext, recognizerOptions, null, null) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity Activity to recognize. + * @param recognizerOptions A LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @param Type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + LuisRecognizerOptions recognizerOptions, Class c) { + return recognizeInternal(dialogContext, activity, recognizerOptions, null, null) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param recognizerOptions LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(TurnContext turnContext, + LuisRecognizerOptions recognizerOptions, Map telemetryProperties, + Map telemetryMetrics) { + return recognizeInternal(turnContext, recognizerOptions, telemetryProperties, telemetryMetrics); + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity Activity to recognize. + * @param recognizerOptions A LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + LuisRecognizerOptions recognizerOptions, Map telemetryProperties, + Map telemetryMetrics) { + return recognizeInternal(dialogContext, activity, recognizerOptions, telemetryProperties, telemetryMetrics); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param recognizerOptions A LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @param Type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(TurnContext turnContext, + LuisRecognizerOptions recognizerOptions, Map telemetryProperties, + Map telemetryMetrics, Class c) { + return recognizeInternal(turnContext, recognizerOptions, telemetryProperties, telemetryMetrics) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Runs an utterance through a recognizer and returns a strongly-typed + * recognizer result. + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity Activity to recognize. + * @param recognizerOptions LuisRecognizerOptions instance to be used by the + * call. This parameter overrides the default + * LuisRecognizerOptions passed in the constructor. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @param Type of result. + * @param c RecognizerConvert implemented class to convert the + * Recognizer Result into. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + public CompletableFuture recognize(DialogContext dialogContext, Activity activity, + LuisRecognizerOptions recognizerOptions, Map telemetryProperties, + Map telemetryMetrics, Class c) { + return recognizeInternal(dialogContext, activity, recognizerOptions, telemetryProperties, telemetryMetrics) + .thenApply(recognizerResult -> convertRecognizerResult(recognizerResult, c)); + } + + /** + * Invoked prior to a LuisResult being logged. + * + * @param recognizerResult The Luis Results for the call. + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + */ + public void onRecognizerResult(RecognizerResult recognizerResult, TurnContext turnContext, + Map telemetryProperties, Map telemetryMetrics) { + Map properties = fillLuisEventProperties(recognizerResult, turnContext, + telemetryProperties); + // Track the event + this.getTelemetryClient().trackEvent(LuisTelemetryConstants.LUIS_RESULT, properties, telemetryMetrics); + } + + /** + * Fills the event properties for LuisResult event for telemetry. + * These properties are logged when the recognizer is called. + * + * @param recognizerResult Last activity sent from user. + * @param turnContext Context object containing information for a single turn of conversation with a user. + * @param telemetryProperties Additional properties to be logged to telemetry with the LuisResult event. + * @return + */ + private Map fillLuisEventProperties(RecognizerResult recognizerResult, TurnContext turnContext, + Map telemetryProperties) { + + Map sortedIntents = sortIntents(recognizerResult); + ArrayList topTwoIntents = new ArrayList<>(); + Iterator> iterator = sortedIntents.entrySet().iterator(); + int intentCounter = 0; + while (iterator.hasNext() && intentCounter < 2) { + intentCounter++; + Map.Entry intent = iterator.next(); + topTwoIntents.add(intent.getKey()); + } + + // Add the intent score and conversation id properties + Map properties = new HashMap<>(); + properties.put(LuisTelemetryConstants.APPLICATION_ID_PROPERTY, + luisRecognizerOptions.getApplication().getApplicationId()); + properties.put(LuisTelemetryConstants.INTENT_PROPERTY, topTwoIntents.size() > 0 ? topTwoIntents.get(0) : ""); + properties.put(LuisTelemetryConstants.INTENT_SCORE_PROPERTY, + topTwoIntents.size() > 0 ? "" + recognizerResult.getIntents().get(topTwoIntents.get(0)).getScore() + : "0.00"); + properties.put(LuisTelemetryConstants.INTENT_2_PROPERTY, topTwoIntents.size() > 1 ? topTwoIntents.get(1) : ""); + properties.put(LuisTelemetryConstants.INTENT_SCORE_2_PROPERTY, + topTwoIntents.size() > 1 ? "" + recognizerResult.getIntents().get(topTwoIntents.get(1)).getScore() + : "0.00"); + properties.put(LuisTelemetryConstants.FROM_ID_PROPERTY, turnContext.getActivity().getFrom().getId()); + + if (recognizerResult.getProperties().containsKey("sentiment")) { + JsonNode sentiment = recognizerResult.getProperties().get("sentiment"); + if (sentiment.has("label")) { + properties.put(LuisTelemetryConstants.SENTIMENT_LABEL_PROPERTY, sentiment.get("label").textValue()); + } + + if (sentiment.has("score")) { + properties.put(LuisTelemetryConstants.SENTIMENT_SCORE_PROPERTY, sentiment.get("score").textValue()); + } + } + + properties.put(LuisTelemetryConstants.ENTITIES_PROPERTY, recognizerResult.getEntities().toString()); + + // Use the LogPersonalInformation flag to toggle logging PII data, text is a + // common example + if (isLogPersonalInformation() && StringUtils.isNotBlank(turnContext.getActivity().getText())) { + properties.put(LuisTelemetryConstants.QUESTION_PROPERTY, turnContext.getActivity().getText()); + } + + // Additional Properties can override "stock" properties. + if (telemetryProperties == null) { + telemetryProperties = new HashMap<>(); + } + + properties.putAll(telemetryProperties); + + return properties; + } + + private T convertRecognizerResult(RecognizerResult recognizerResult, Class clazz) { + T result; + try { + result = clazz.newInstance(); + result.convert(recognizerResult); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException( + String.format("Exception thrown when converting " + "Recognizer Result to strongly typed: %s : %s", + clazz.getName(), e.getMessage())); + } + return result; + } + + /** + * Returns a RecognizerResult object. This method will call the internal + * recognize implementation of the Luis Recognizer Options. + */ + private CompletableFuture recognizeInternal(TurnContext turnContext, + LuisRecognizerOptions options, Map telemetryProperties, + Map telemetryMetrics) { + LuisRecognizerOptions predictionOptionsToRun = options == null ? luisRecognizerOptions : options; + return predictionOptionsToRun.recognizeInternal(turnContext).thenApply(recognizerResult -> { + onRecognizerResult(recognizerResult, turnContext, telemetryProperties, telemetryMetrics); + return recognizerResult; + }); + } + + /** + * Returns a RecognizerResult object. This method will call the internal + * recognize implementation of the Luis Recognizer Options. + */ + private CompletableFuture recognizeInternal(DialogContext dialogContext, Activity activity, + LuisRecognizerOptions options, Map telemetryProperties, + Map telemetryMetrics) { + LuisRecognizerOptions predictionOptionsToRun = options == null ? luisRecognizerOptions : options; + return predictionOptionsToRun.recognizeInternal(dialogContext, activity).thenApply(recognizerResult -> { + onRecognizerResult(recognizerResult, dialogContext.getContext(), telemetryProperties, telemetryMetrics); + return recognizerResult; + }); + } + + private Map sortIntents(RecognizerResult recognizerResult) { + Map sortedIntents = new LinkedHashMap<>(); + recognizerResult.getIntents().entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.comparingDouble(IntentScore::getScore).reversed())) + .forEachOrdered(x -> sortedIntents.put(x.getKey(), x.getValue())); + return sortedIntents; + } +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptions.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptions.java new file mode 100644 index 000000000..d24aa0030 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptions.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.schema.Activity; +import java.util.concurrent.CompletableFuture; + +/** + * Abstract class to enforce the Strategy pattern consumed by the Luis + * Recognizer through the options selected. + * + */ +public abstract class LuisRecognizerOptions { + + /** + * Initializes an instance of the LuisRecognizerOptions implementation. + * + * @param application An instance of LuisApplication". + */ + protected LuisRecognizerOptions(LuisApplication application) { + if (application == null) { + throw new IllegalArgumentException("Luis Application may not be null"); + } + this.application = application; + } + + /** + * Luis Application instance. + */ + private LuisApplication application; + + /** + * Bot Telemetry Client instance. + */ + private BotTelemetryClient telemetryClient = new NullBotTelemetryClient(); + + /** + * Controls if personal information should be sent as telemetry. + */ + private boolean logPersonalInformation = false; + + /** + * Controls if full results from the LUIS API should be returned with the + * recognizer result. + */ + private boolean includeAPIResults = false; + + /** + * Gets the Luis Application instance. + * + * @return The Luis Application instance used with this Options. + */ + public LuisApplication getApplication() { + return application; + } + + /** + * Gets the currently configured Bot Telemetry Client that logs the LuisResult + * event. + * + * @return The Bot Telemetry Client. + */ + public BotTelemetryClient getTelemetryClient() { + return telemetryClient; + } + + /** + * Sets the Bot Telemetry Client to log telemetry with. + * + * @param telemetryClient A Bot Telemetry Client instance + */ + public void setTelemetryClient(BotTelemetryClient telemetryClient) { + this.telemetryClient = telemetryClient; + } + + /** + * Indicates if personal information should be sent as telemetry. + * + * @return value boolean value to control personal information logging. + */ + public boolean isLogPersonalInformation() { + return logPersonalInformation; + } + + /** + * Indicates if personal information should be sent as telemetry. + * + * @param logPersonalInformation to set personal information logging preference. + */ + public void setLogPersonalInformation(boolean logPersonalInformation) { + this.logPersonalInformation = logPersonalInformation; + } + + /** + * Indicates if full results from the LUIS API should be returned with the + * recognizer result. + * + * @return boolean value showing preference on LUIS API full response added to + * recognizer result. + */ + public boolean isIncludeAPIResults() { + return includeAPIResults; + } + + /** + * Indicates if full results from the LUIS API should be returned with the + * recognizer result. + * + * @param includeAPIResults to set full Luis API response to be added to the + * recognizer result. + */ + public void setIncludeAPIResults(boolean includeAPIResults) { + this.includeAPIResults = includeAPIResults; + } + + /** + * Implementation of the Luis API http call and result processing. This is + * intended to follow a Strategy pattern and should only be consumed through the + * LuisRecognizer class. + * + * @param turnContext used to extract the text utterance to be sent to Luis. + * @return Recognizer Result populated by the Luis response. + */ + abstract CompletableFuture recognizeInternal(TurnContext turnContext); + + /** + * Implementation of the Luis API http call and result processing. This is + * intended to follow a Strategy pattern and should only be consumed through the + * LuisRecognizer class. + * + * @param context Dialog Context to extract turn context. + * @param activity to extract the text utterance to be sent to Luis. + * @return Recognizer Result populated by the Luis response. + */ + abstract CompletableFuture recognizeInternal(DialogContext context, Activity activity); +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java new file mode 100644 index 000000000..4a6a56682 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java @@ -0,0 +1,732 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.IntentScore; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.Recognizer; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +import org.apache.commons.lang3.StringUtils; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** + * Luis Recognizer Options for V3 LUIS Runtime. + * + */ +public class LuisRecognizerOptionsV3 extends LuisRecognizerOptions { + private final HashSet dateSubtypes = new HashSet<>( + Arrays.asList("date", "daterange", "datetime", "datetimerange", "duration", "set", "time", "timerange")); + + private final HashSet geographySubtypes = new HashSet<>( + Arrays.asList("poi", "city", "countryRegion", "continent", "state")); + + private final String metadataKey = "$instance"; + + /** + * DatetimeV2 offset. The format for the datetimeReference is ISO 8601. + */ + private String dateTimeReference = null; + + /** + * Dynamic lists used to recognize entities for a particular query. + */ + private List dynamicLists = null; + + /** + * External entities recognized in query. + */ + private List externalEntities = null; + + /** + * External entity recognizer to recognize external entities to pass to LUIS. + */ + private Recognizer externalEntityRecognizer = null; + + /** + * Value indicating whether all intents come back or only the top one. True for + * returning all intents. + */ + private boolean includeAllIntents = false; + + /** + * Value indicating whether or not instance data should be included in response. + */ + private boolean includeInstanceData = true; + + /** + * Value indicating whether queries should be logged in LUIS. If queries should + * be logged in LUIS in order to help build better models through active + * learning + */ + private boolean log = true; + + /** + * Value indicating whether external entities should override other means of + * recognizing entities. True if external entities should be preferred to the + * results from LUIS models + */ + private boolean preferExternalEntities = true; + + /** + * The LUIS slot to use for the application. By default this uses the production + * slot. You can find other standard slots in LuisSlot. If you specify a + * Version, then a private version of the application is used instead of a slot. + */ + private String slot = LuisSlot.PRODUCTION; + + /** + * The specific version of the application to access. LUIS supports versions and + * this is the version to use instead of a slot. If this is specified, then the + * Slot is ignored. + */ + private String version = null; + + /** + * The HttpClient instance to use for http calls against the LUIS endpoint. + */ + private OkHttpClient httpClient = new OkHttpClient(); + + /** + * The value type for a LUIS trace activity. + */ + public static final String LUIS_TRACE_TYPE = "https://www.luis.ai/schemas/trace"; + + /** + * The context label for a LUIS trace activity. + */ + public static final String LUIS_TRACE_LABEL = "LuisV3 Trace"; + + /** + * Gets External entity recognizer to recognize external entities to pass to + * LUIS. + * + * @return externalEntityRecognizer + */ + public Recognizer getExternalEntityRecognizer() { + return externalEntityRecognizer; + } + + /** + * Sets External entity recognizer to recognize external entities to pass to + * LUIS. + * + * @param externalEntityRecognizer External Recognizer instance. + */ + public void setExternalEntityRecognizer(Recognizer externalEntityRecognizer) { + this.externalEntityRecognizer = externalEntityRecognizer; + } + + /** + * Gets indicating whether all intents come back or only the top one. True for + * returning all intents. + * + * @return True for returning all intents. + */ + public boolean isIncludeAllIntents() { + return includeAllIntents; + } + + /** + * Sets indicating whether all intents come back or only the top one. + * + * @param includeAllIntents True for returning all intents. + */ + public void setIncludeAllIntents(boolean includeAllIntents) { + this.includeAllIntents = includeAllIntents; + } + + /** + * Gets value indicating whether or not instance data should be included in + * response. + * + * @return True if instance data should be included in response. + */ + public boolean isIncludeInstanceData() { + return includeInstanceData; + } + + /** + * Sets value indicating whether or not instance data should be included in + * response. + * + * @param includeInstanceData True if instance data should be included in + * response. + */ + public void setIncludeInstanceData(boolean includeInstanceData) { + this.includeInstanceData = includeInstanceData; + } + + /** + * Value indicating whether queries should be logged in LUIS. If queries should + * be logged in LUIS in order to help build better models through active + * learning + * + * @return True if queries should be logged in LUIS. + */ + public boolean isLog() { + return log; + } + + /** + * Value indicating whether queries should be logged in LUIS. If queries should + * be logged in LUIS in order to help build better models through active + * learning. + * + * @param log True if queries should be logged in LUIS. + */ + public void setLog(boolean log) { + this.log = log; + } + + /** + * Returns Dynamic lists used to recognize entities for a particular query. + * + * @return Dynamic lists used to recognize entities for a particular query + */ + public List getDynamicLists() { + return dynamicLists; + } + + /** + * Sets Dynamic lists used to recognize entities for a particular query. + * + * @param dynamicLists to recognize entities for a particular query. + */ + public void setDynamicLists(List dynamicLists) { + this.dynamicLists = dynamicLists; + } + + /** + * Gets External entities to be recognized in query. + * + * @return External entities to be recognized in query. + */ + public List getExternalEntities() { + return externalEntities; + } + + /** + * Sets External entities to be recognized in query. + * + * @param externalEntities External entities to be recognized in query. + */ + public void setExternalEntities(List externalEntities) { + this.externalEntities = externalEntities; + } + + /** + * Gets value indicating whether external entities should override other means + * of recognizing entities. + * + * @return True if external entities should be preferred to the results from + * LUIS models. + */ + public boolean isPreferExternalEntities() { + return preferExternalEntities; + } + + /** + * Sets value indicating whether external entities should override other means + * of recognizing entities. + * + * @param preferExternalEntities True if external entities should be preferred + * to the results from LUIS models. + */ + public void setPreferExternalEntities(boolean preferExternalEntities) { + this.preferExternalEntities = preferExternalEntities; + } + + /** + * Gets datetimeV2 offset. The format for the datetimeReference is ISO 8601. + * + * @return The format for the datetimeReference in ISO 8601. + */ + public String getDateTimeReference() { + return dateTimeReference; + } + + /** + * Sets datetimeV2 offset. + * + * @param dateTimeReference The format for the datetimeReference is ISO 8601. + */ + public void setDateTimeReference(String dateTimeReference) { + this.dateTimeReference = dateTimeReference; + } + + /** + * Gets the LUIS slot to use for the application. By default this uses the + * production slot. You can find other standard slots in LuisSlot. If you + * specify a Version, then a private version of the application is used instead + * of a slot. + * + * @return LuisSlot constant. + */ + public String getSlot() { + return slot; + } + + /** + * Sets the LUIS slot to use for the application. By default this uses the + * production slot. You can find other standard slots in LuisSlot. If you + * specify a Version, then a private version of the application is used instead + * of a slot. + * + * @param slot LuisSlot value to use. + */ + public void setSlot(String slot) { + this.slot = slot; + } + + /** + * Gets the specific version of the application to access. LUIS supports + * versions and this is the version to use instead of a slot. If this is + * specified, then the Slot is ignored. + * + * @return Luis application version to Query. + */ + public String getVersion() { + return version; + } + + /** + * Sets the specific version of the application to access. LUIS supports + * versions and this is the version to use instead of a slot. + * + * @param version Luis Application version. If this is specified, then the Slot + * is ignored. + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Gets whether the http client. + * + * @return OkHttpClient used to query the Luis Service. + */ + public OkHttpClient getHttpClient() { + return httpClient; + } + + /** + * Sets the http client. + * + * @param httpClient to use for Luis Service http calls. + */ + public void setHttpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * Initializes a new instance of the LuisRecognizerOptionsV3. + * + * @param application Luis Application instance to query. + */ + public LuisRecognizerOptionsV3(LuisApplication application) { + super(application); + } + + /** + * Internal implementation of the http request to the LUIS service and parsing + * of the response to a Recognizer Result instance. + * + * @param dialogContext Context Object. + * @param activity Activity object to extract the utterance. + */ + @Override + CompletableFuture recognizeInternal(DialogContext dialogContext, Activity activity) { + if (externalEntityRecognizer == null) { + return recognizeInternal(dialogContext.getContext(), activity.getText()); + } + + // call external entity recognizer + List originalExternalEntities = externalEntities; + return externalEntityRecognizer.recognize(dialogContext, activity).thenCompose(matches -> { + if (matches.getEntities() == null || matches.getEntities().toString().equals("{}")) { + return recognizeInternal(dialogContext.getContext(), activity.getText()); + } + + List recognizerExternalEntities = new ArrayList<>(); + JsonNode entities = matches.getEntities(); + JsonNode instance = entities.get("$instance"); + + if (instance == null) { + return recognizeInternal(dialogContext.getContext(), activity.getText()); + } + + Iterator> instanceEntitiesIterator = instance.fields(); + + while (instanceEntitiesIterator.hasNext()) { + Map.Entry property = instanceEntitiesIterator.next(); + + if (property.getKey().equals("text") || property.getKey().equals("$instance")) { + continue; + } + + ArrayNode instances = (ArrayNode) instance.get(property.getKey()); + ArrayNode values = (ArrayNode) property.getValue(); + + if (instances == null || values == null || instances.size() != values.size()) { + continue; + } + + for (JsonNode childInstance : values) { + if (childInstance != null && childInstance.has("startIndex") && childInstance.has("endIndex")) { + int start = childInstance.get("startIndex").asInt(); + int end = childInstance.get("endIndex").asInt(); + recognizerExternalEntities + .add(new ExternalEntity(property.getKey(), start, end - start, property.getValue())); + } + } + recognizerExternalEntities + .addAll(originalExternalEntities == null ? new ArrayList<>() : originalExternalEntities); + externalEntities = recognizerExternalEntities; + } + + return recognizeInternal(dialogContext.getContext(), activity.getText()).thenApply(recognizerResult -> { + externalEntities = originalExternalEntities; + return recognizerResult; + }); + }); + } + + /** + * Internal implementation of the http request to the LUIS service and parsing + * of the response to a Recognizer Result instance. + * + * @param turnContext Context Object. + */ + @Override + CompletableFuture recognizeInternal(TurnContext turnContext) { + return recognizeInternal(turnContext, turnContext.getActivity().getText()); + } + + private Request buildRequest(RequestBody body) { + StringBuilder path = new StringBuilder(getApplication().getEndpoint()); + path.append(String.format("/luis/prediction/v3.0/apps/%s", getApplication().getApplicationId())); + + if (version == null) { + path.append(String.format("/slots/%s/predict", slot)); + } else { + path.append(String.format("/versions/%s/predict", version)); + } + + HttpUrl.Builder httpBuilder = HttpUrl.parse(path.toString()).newBuilder(); + + httpBuilder.addQueryParameter("verbose", Boolean.toString(includeInstanceData)); + httpBuilder.addQueryParameter("log", Boolean.toString(log)); + httpBuilder.addQueryParameter("show-all-intents", Boolean.toString(includeAllIntents)); + + Request.Builder requestBuilder = new Request.Builder().url(httpBuilder.build()) + .addHeader("Ocp-Apim-Subscription-Key", getApplication().getEndpointKey()).post(body); + return requestBuilder.build(); + } + + private RequestBody buildRequestBody(String utterance) throws JsonProcessingException { + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectNode content = JsonNodeFactory.instance.objectNode().put("query", utterance); + ObjectNode queryOptions = JsonNodeFactory.instance.objectNode().put("preferExternalEntities", + preferExternalEntities); + + if (StringUtils.isNotBlank(dateTimeReference)) { + queryOptions.put("datetimeReference", dateTimeReference); + } + + content.set("options", queryOptions); + + if (dynamicLists != null) { + content.set("dynamicLists", mapper.valueToTree(dynamicLists)); + } + + if (externalEntities != null) { + for (ExternalEntity entity : externalEntities) { + entity.validate(); + } + content.set("externalEntities", mapper.valueToTree(externalEntities)); + } + + String contentAsText = mapper.writeValueAsString(content); + return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), contentAsText); + } + + private CompletableFuture recognizeInternal(TurnContext turnContext, String utterance) { + + RecognizerResult recognizerResult; + JsonNode luisResponse = null; + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + if (utterance == null || utterance.isEmpty()) { + recognizerResult = new RecognizerResult(); + recognizerResult.setText(utterance); + } else { + try { + Request request = buildRequest(buildRequestBody(utterance)); + Response response = httpClient.newCall(request).execute(); + luisResponse = mapper.readTree(response.body().string()); + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + luisResponse.toString()); + } + + } catch (IOException e) { + CompletableFuture exceptionResult = new CompletableFuture<>(); + exceptionResult.completeExceptionally(e); + return exceptionResult; + } + + JsonNode prediction = luisResponse.get("prediction"); + recognizerResult = new RecognizerResult(); + recognizerResult.setText(utterance); + if (prediction.get("alteredQuery") != null) { + recognizerResult.setAlteredText(prediction.get("alteredQuery").asText()); + } + + recognizerResult.setIntents(getIntents(prediction)); + recognizerResult.setEntities(getEntities(prediction)); + + addProperties(prediction, recognizerResult); + if (isIncludeAPIResults()) { + recognizerResult.getProperties().put("luisResult", luisResponse); + } + + if (includeInstanceData && recognizerResult.getEntities().get(metadataKey) == null) { + ((ObjectNode) recognizerResult.getEntities()).putObject(metadataKey); + } + } + + return sendTraceActivity(recognizerResult, luisResponse, turnContext).thenApply(v -> recognizerResult); + + } + + private Map getIntents(JsonNode prediction) { + Map intents = new LinkedHashMap<>(); + + JsonNode intentsObject = prediction.get("intents"); + if (intentsObject == null) { + return intents; + } + + for (Iterator> it = intentsObject.fields(); it.hasNext();) { + Map.Entry intent = it.next(); + double score = intent.getValue().get("score").asDouble(); + String intentName = intent.getKey().replace(".", "_").replace(" ", "_"); + IntentScore intentScore = new IntentScore(); + intentScore.setScore(score); + intents.put(intentName, intentScore); + } + + return intents; + } + + private String normalizeEntity(String entity) { + // Type::Role -> Role + String[] type = entity.split(":"); + return type[type.length - 1].replace(".", "_").replace(" ", "_"); + } + + private JsonNode getEntities(JsonNode prediction) { + if (prediction.get("entities") == null) { + return JsonNodeFactory.instance.objectNode(); + } + + return mapEntitiesRecursive(prediction.get("entities"), false); + } + + // Exact Port from C# + private JsonNode mapEntitiesRecursive(JsonNode source, boolean inInstance) { + JsonNode result = source; + if (!source.isArray() && source.isObject()) { + ObjectNode nobj = JsonNodeFactory.instance.objectNode(); + // Fix datetime by reverting to simple timex + JsonNode obj = source; + JsonNode type = source.get("type"); + + if (!inInstance && type != null && dateSubtypes.contains(type.asText())) { + JsonNode timexs = obj.get("values"); + ArrayNode arr = JsonNodeFactory.instance.arrayNode(); + if (timexs != null) { + Set unique = new HashSet<>(); + + for (JsonNode elt : timexs) { + unique.add(elt.get("timex").textValue()); + } + + for (String timex : unique) { + arr.add(timex); + } + + nobj.set("timex", arr); + } + + nobj.set("type", type); + } else { + // Map or remove properties + Iterator> nodes = obj.fields(); + while (nodes.hasNext()) { + Map.Entry property = (Map.Entry) nodes.next(); + String name = normalizeEntity(property.getKey()); + boolean isArray = property.getValue().isArray(); + boolean isString = property.getValue().isTextual(); + boolean isInt = property.getValue().isInt(); + JsonNode val = mapEntitiesRecursive(property.getValue(), inInstance || name.equals(metadataKey)); + + if (name.equals("datetime") && isArray) { + nobj.set("datetimeV1", val); + } else if (name.equals("datetimeV2") && isArray) { + nobj.set("datetime", val); + } else if (inInstance) { + // Correct $instance issues + if (name.equals("length") && isInt) { + int value = property.getValue().intValue(); + if (obj.get("startIndex") != null) { + value += obj.get("startIndex").intValue(); + } + nobj.put("endIndex", value); + } else if (!((isInt && name.equals("modelTypeId")) || // NOPMD + (isString && name.equals("role"))) // NOPMD + ) { // NOPMD + nobj.set(name, val); + } + } else { + // Correct non-$instance values + if (name.equals("unit") && isString) { + nobj.set("units", val); + } else { + nobj.set(name, val); + } + } + } + } + + result = nobj; + } else if (source.isArray()) { + JsonNode arr = source; + ArrayNode narr = JsonNodeFactory.instance.arrayNode(); + for (JsonNode elt : arr) { + // Check if element is geographyV2 + String isGeographyV2 = ""; + + Iterator> nodes = elt.fields(); + while (nodes.hasNext()) { + Map.Entry props = (Map.Entry) nodes.next(); + + if (props == null) { + break; + } + + if (props.getKey().contains("type") && geographySubtypes.contains(props.getValue().textValue())) { + isGeographyV2 = props.getValue().textValue(); + break; + } + } + + if (!inInstance && !isGeographyV2.isEmpty()) { + ObjectNode geoEntity = JsonNodeFactory.instance.objectNode(); + nodes = elt.fields(); + while (nodes.hasNext()) { + Map.Entry tokenProp = (Map.Entry) nodes.next(); + + if (tokenProp.getKey().contains("value")) { + geoEntity.set("location", tokenProp.getValue()); + } + } + + geoEntity.put("type", isGeographyV2); + narr.add(geoEntity); + } else { + narr.add(mapEntitiesRecursive(elt, inInstance)); + } + } + result = narr; + } + + return result; + } + + private void addProperties(JsonNode prediction, RecognizerResult result) { + JsonNode sentiment = prediction.get("sentiment"); + if (sentiment != null) { + ObjectNode sentimentNode = JsonNodeFactory.instance.objectNode(); + sentimentNode.set("label", sentiment.get("label")); + sentimentNode.set("score", sentiment.get("score")); + result.getProperties().put("sentiment", sentimentNode); + } + } + + private CompletableFuture sendTraceActivity(RecognizerResult recognizerResult, + JsonNode luisResponse, TurnContext turnContext) { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + try { + ObjectNode traceInfo = JsonNodeFactory.instance.objectNode(); + traceInfo.put("recognizerResult", + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(recognizerResult)); + traceInfo.set("luisResult", luisResponse); + traceInfo.set("luisModel", + JsonNodeFactory.instance.objectNode().put("ModelId", getApplication().getApplicationId())); + + ObjectNode luisOptions = JsonNodeFactory.instance.objectNode(); + luisOptions.put("includeAllIntents", includeAllIntents); + luisOptions.put("includeInstanceData", includeInstanceData); + luisOptions.put("log", log); + luisOptions.put("preferExternalEntities", preferExternalEntities); + luisOptions.put("dateTimeReference", dateTimeReference); + luisOptions.put("slot", slot); + luisOptions.put("version", version); + + if (externalEntities != null) { + ArrayNode externalEntitiesNode = JsonNodeFactory.instance.arrayNode(); + for (ExternalEntity e : externalEntities) { + externalEntitiesNode.add(mapper.valueToTree(e)); + } + luisOptions.put("externalEntities", externalEntitiesNode); + } + + if (dynamicLists != null) { + ArrayNode dynamicListNode = JsonNodeFactory.instance.arrayNode(); + for (DynamicList e : dynamicLists) { + dynamicListNode.add(mapper.valueToTree(e)); + } + luisOptions.put("dynamicLists", dynamicListNode); + } + + traceInfo.set("luisOptions", luisOptions); + + return turnContext.sendActivity( + Activity.createTraceActivity("LuisRecognizer", LUIS_TRACE_TYPE, traceInfo, LUIS_TRACE_LABEL)); + + } catch (IOException e) { + CompletableFuture exceptionResult = new CompletableFuture<>(); + exceptionResult.completeExceptionally(e); + return exceptionResult; + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisSlot.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisSlot.java new file mode 100644 index 000000000..20d45a517 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisSlot.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +/** + * Utility class to set the Luis endpoint Slot. + * + */ +public final class LuisSlot { + + // Not Called + private LuisSlot() { + + } + + /** + * Production slot on LUIS. + */ + public static final String PRODUCTION = "production"; + + /** + * Staging slot on LUIS. + */ + public static final String STAGING = "staging"; +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisTelemetryConstants.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisTelemetryConstants.java new file mode 100644 index 000000000..550b7bdcf --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisTelemetryConstants.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +/** + * Utility class to set the telemetry values for the Luis Recognizer. + * + */ +public final class LuisTelemetryConstants { + + private LuisTelemetryConstants() { + + } + + /** + * The Key used when storing a LUIS Result in a custom event within telemetry. + */ + public static final String LUIS_RESULT = "LuisResult"; // Event name + + /** + * The Key used when storing a LUIS app ID in a custom event within telemetry. + */ + public static final String APPLICATION_ID_PROPERTY = "applicationId"; + + /** + * The Key used when storing a LUIS intent in a custom event within telemetry. + */ + public static final String INTENT_PROPERTY = "intent"; + + /** + * The Key used when storing a LUIS intent score in a custom event within + * telemetry. + */ + public static final String INTENT_SCORE_PROPERTY = "intentScore"; + + /** + * The Key used when storing a LUIS intent in a custom event within telemetry. + */ + public static final String INTENT_2_PROPERTY = "intent2"; + + /** + * The Key used when storing a LUIS intent score in a custom event within + * telemetry. + */ + public static final String INTENT_SCORE_2_PROPERTY = "intentScore2"; + + /** + * The Key used when storing LUIS entities in a custom event within telemetry. + */ + public static final String ENTITIES_PROPERTY = "entities"; + + /** + * The Key used when storing the LUIS query in a custom event within telemetry. + */ + public static final String QUESTION_PROPERTY = "question"; + + /** + * The Key used when storing an Activity ID in a custom event within telemetry. + */ + public static final String ACTIVITY_ID_PROPERTY = "activityId"; + + /** + * The Key used when storing a sentiment label in a custom event within + * telemetry. + */ + public static final String SENTIMENT_LABEL_PROPERTY = "sentimentLabel"; + + /** + * The Key used when storing a LUIS sentiment score in a custom event within + * telemetry. + */ + public static final String SENTIMENT_SCORE_PROPERTY = "sentimentScore"; + + /** + * The Key used when storing the FromId in a custom event within telemetry. + */ + public static final String FROM_ID_PROPERTY = "fromId"; +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/TelemetryRecognizer.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/TelemetryRecognizer.java new file mode 100644 index 000000000..19eba4068 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/TelemetryRecognizer.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.Recognizer; +import com.microsoft.bot.builder.RecognizerConvert; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Telemetry Recognizer to enforce controls and properties on telemetry logged. + * Recognizer with Telemetry support. + * + */ +public abstract class TelemetryRecognizer implements Recognizer { + + private boolean logPersonalInformation; + + private BotTelemetryClient telemetryClient; + + /** + * Indicates if personal information should be sent as telemetry. + * + * @return value boolean value to control personal information logging. + */ + public boolean isLogPersonalInformation() { + return logPersonalInformation; + } + + /** + * Indicates if personal information should be sent as telemetry. + * + * @param logPersonalInformation to set personal information logging preference. + */ + protected void setLogPersonalInformation(boolean logPersonalInformation) { + this.logPersonalInformation = logPersonalInformation; + } + + /** + * Gets the currently configured Bot Telemetry Client that logs the LuisResult + * event. + * + * @return The Bot Telemetry Client. + */ + protected BotTelemetryClient getTelemetryClient() { + return telemetryClient; + } + + /** + * Sets the currently configured Bot Telemetry Client that logs the LuisResult + * event. + * + * @param telemetryClient Bot Telemetry Client. + */ + public void setTelemetryClient(BotTelemetryClient telemetryClient) { + this.telemetryClient = telemetryClient; + } + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + abstract CompletableFuture recognize(TurnContext turnContext, + Map telemetryProperties, Map telemetryMetrics); + + /** + * Return results of the analysis (Suggested actions and intents). + * + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @param Result type. + * @param c The recognition result type class + * @return The LUIS results of the analysis of the current message text in the + * current turn's context activity. + */ + abstract CompletableFuture recognize(TurnContext turnContext, + Map telemetryProperties, Map telemetryMetrics, Class c); + +} diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/package-info.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/package-info.java new file mode 100644 index 000000000..6927f945d --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.ai.luis. + */ +@Deprecated +package com.microsoft.bot.ai.luis; diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisApplicationTests.java b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisApplicationTests.java new file mode 100644 index 000000000..4522a3c92 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisApplicationTests.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import org.junit.Assert; +import org.junit.Test; + +public class LuisApplicationTests { + String validUUID = "b31aeaf3-3511-495b-a07f-571fc873214b"; + String invalidUUID = "0000"; + String validEndpoint = "https://www.test.com"; + String invalidEndpoint = "www.test.com"; + + @Test + public void invalidSubscriptionKey() { + + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisApplication lA = new LuisApplication( + validUUID, + invalidUUID, + validEndpoint); + }); + + String expectedMessage = String.format("%s is not a valid LUIS subscription key.", invalidUUID); + String actualMessage = exception.getMessage(); + + Assert.assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + public void invalidApplicationId () { + + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisApplication lA = new LuisApplication( + invalidUUID, + validUUID, + validEndpoint); + }); + + String expectedMessage = String.format("%s is not a valid LUIS application id.", invalidUUID); + String actualMessage = exception.getMessage(); + + Assert.assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + public void invalidEndpoint() { + + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisApplication lA = new LuisApplication( + validUUID, + validUUID, + invalidEndpoint); + }); + + String expectedMessage = String.format("%s is not a valid LUIS endpoint.", invalidEndpoint); + String actualMessage = exception.getMessage(); + + Assert.assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + public void createsNewLuisApplication() { + + LuisApplication lA = new LuisApplication( + validUUID, + validUUID, + validEndpoint + ); + + Assert.assertTrue(lA.getApplicationId().equals(validUUID)); + Assert.assertTrue(lA.getEndpointKey().equals(validUUID)); + Assert.assertTrue(lA.getEndpoint().equals(validEndpoint)); + } + + @Test + public void createsNewLuisApplicationFromURL() { + String url = "https://westus.api.cognitive.microsoft.com/luis/prediction/v3.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b/slots/production/predict?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291"; + LuisApplication lA = new LuisApplication(url); + + Assert.assertTrue(lA.getApplicationId().equals("b31aeaf3-3511-495b-a07f-571fc873214b")); + Assert.assertTrue(lA.getEndpointKey().equals("048ec46dc58e495482b0c447cfdbd291")); + Assert.assertTrue(lA.getEndpoint().equals("https://westus.api.cognitive.microsoft.com")); + } + + @Test + public void listApplicationFromLuisEndpointBadArguments() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisApplication lA = new LuisApplication("this.is.not.a.uri"); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisApplication lA = new LuisApplication("https://westus.api.cognitive.microsoft.com/luis/v3.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&q="); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisApplication lA = new LuisApplication("https://westus.api.cognitive.microsoft.com?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q="); + }); + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3Tests.java b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3Tests.java new file mode 100644 index 000000000..f60e0c562 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3Tests.java @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.Recognizer; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + + +@RunWith(MockitoJUnitRunner.class) +public class LuisRecognizerOptionsV3Tests { + + @Mock + DialogContext dC; + + @Mock + Recognizer recognizer; + + @Mock + TurnContext turnContext; + + // Set this values to test against the service + String applicationId = "b31aeaf3-3511-495b-a07f-571fc873214b"; + String subscriptionKey = "b31aeaf3-3511-495b-a07f-571fc873214b"; + boolean mockLuisResponse = true; + + @Test + public void shouldParseLuisResponsesCorrectly_TurnContextPassed() { + String[] files = { + "Composite1.json", + "Composite2.json", + "Composite3.json", + "DateTimeReference.json", + "DynamicListsAndList.json", + "ExternalEntitiesAndBuiltin.json", + "ExternalEntitiesAndComposite.json", + "ExternalEntitiesAndList.json", + "ExternalEntitiesAndRegex.json", + "ExternalEntitiesAndSimple.json", + "ExternalEntitiesAndSimpleOverride.json", + "GeoPeopleOrdinal.json", + "Minimal.json", +// TODO: This is disabled until the bug requiring instance data for geo is fixed. +// "MinimalWithGeo.json", + "NoEntitiesInstanceTrue.json", + "Patterns.json", + "Prebuilt.json", + "roles.json", + "TraceActivity.json", + "Typed.json", + "TypedPrebuilt.json" + }; + + for (String file : files) { + shouldParseLuisResponsesCorrectly_TurnContextPassed(file); + reset(turnContext); + } + } + + private void shouldParseLuisResponsesCorrectly_TurnContextPassed(String fileName) { + RecognizerResult result = null, expected = null; + MockWebServer mockWebServer = new MockWebServer(); + + try { + // Get Oracle file + String content = readFileContent("/src/test/java/com/microsoft/bot/ai/luis/testdata/" + fileName); + + //Extract V3 response + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode testData = mapper.readTree(content); + JsonNode v3SettingsAndResponse = testData.get("v3"); + JsonNode v3Response = v3SettingsAndResponse.get("response"); + + //Extract V3 Test Settings + JsonNode testSettings = v3SettingsAndResponse.get("options"); + + // Set mock response in MockWebServer + StringBuilder pathToMock = new StringBuilder("/luis/prediction/v3.0/apps/"); + String url = buildUrl(pathToMock, testSettings); + String endpoint = ""; + if (this.mockLuisResponse) { + endpoint = String.format( + "http://localhost:%s", + initializeMockServer( + mockWebServer, + v3Response, + url).port()); + } + + // Set LuisRecognizerOptions data + LuisRecognizerOptionsV3 v3 = buildTestRecognizer(endpoint, testSettings); + + // Run test + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(testData.get("text").asText()); + activity.setChannelId("EmptyContext"); + doReturn(activity) + .when(turnContext) + .getActivity(); + + doReturn(CompletableFuture.completedFuture(new ResourceResponse())) + .when(turnContext) + .sendActivity(any(Activity.class)); + + result = v3.recognizeInternal(turnContext).get(); + + // Build expected result + expected = mapper.readValue(content, RecognizerResult.class); + Map properties = expected.getProperties(); + properties.remove("v2"); + properties.remove("v3"); + + assertEquals(mapper.writeValueAsString(expected), mapper.writeValueAsString(result)); + + RecordedRequest request = mockWebServer.takeRequest(); + assertEquals(String.format("POST %s HTTP/1.1", pathToMock.toString()), request.getRequestLine()); + assertEquals(pathToMock.toString(), request.getPath()); + + verify(turnContext, times(1)).sendActivity(any (Activity.class)); + + } catch (InterruptedException | ExecutionException | IOException e) { + e.printStackTrace(); + assertFalse(true); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void shouldBuildExternalEntities_DialogContextPassed_ExternalRecognizer() { + MockWebServer mockWebServer = new MockWebServer(); + + try { + // Get Oracle file + String content = readFileContent("/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalRecognizer.json"); + + //Extract V3 response + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode testData = mapper.readTree(content); + JsonNode v3SettingsAndResponse = testData.get("v3"); + JsonNode v3Response = v3SettingsAndResponse.get("response"); + + //Extract V3 Test Settings + JsonNode testSettings = v3SettingsAndResponse.get("options"); + + // Set mock response in MockWebServer + StringBuilder pathToMock = new StringBuilder("/luis/prediction/v3.0/apps/"); + String url = buildUrl(pathToMock, testSettings); + String endpoint = String.format( + "http://localhost:%s", + initializeMockServer( + mockWebServer, + v3Response, + url).port()); + + // Set LuisRecognizerOptions data + LuisRecognizerOptionsV3 v3 = buildTestRecognizer(endpoint, testSettings); + v3.setExternalEntityRecognizer(recognizer); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(testData.get("text").asText()); + activity.setChannelId("EmptyContext"); + + doReturn(CompletableFuture.completedFuture(new ResourceResponse())) + .when(turnContext) + .sendActivity(any(Activity.class)); + + when(dC.getContext()).thenReturn(turnContext); + + doReturn(CompletableFuture.supplyAsync(() -> { + RecognizerResult recognizerResult = new RecognizerResult(); + recognizerResult.setEntities(testSettings.get("ExternalRecognizerResult")); + return recognizerResult; + })) + .when(recognizer) + .recognize(any(DialogContext.class), any(Activity.class)); + + v3.recognizeInternal(dC, activity).get(); + + RecordedRequest request = mockWebServer.takeRequest(); + String resultBody = request.getBody().readUtf8(); + assertEquals("{\"query\":\"deliver 35 WA to repent harelquin\"," + + "\"options\":{\"preferExternalEntities\":true}," + + "\"externalEntities\":[{\"entityName\":\"Address\",\"startIndex\":17,\"entityLength\":16," + + "\"resolution\":[{\"endIndex\":33,\"modelType\":\"Composite Entity Extractor\"," + + "\"resolution\":{\"number\":[3],\"State\":[\"France\"]}," + + "\"startIndex\":17,\"text\":\"repent harelquin\",\"type\":\"Address\"}]}]}", + resultBody); + + } catch (InterruptedException | ExecutionException | IOException e) { + e.printStackTrace(); + assertFalse(true); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public static TurnContext createContext(String message) { + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(message); + activity.setChannelId("EmptyContext"); + + return new TurnContextImpl(new NotImplementedAdapter(), activity); + } + + private static class NotImplementedAdapter extends BotAdapter { + @Override + public CompletableFuture sendActivities( + TurnContext context, + List activities + ) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture updateActivity( + TurnContext context, + Activity activity + ) { + throw new RuntimeException(); + } + + @Override + public CompletableFuture deleteActivity( + TurnContext context, + ConversationReference reference + ) { + throw new RuntimeException(); + } + } + + private String readFileContent (String pathToFile) throws IOException { + String path = Paths.get("").toAbsolutePath().toString(); + File file = new File(path + pathToFile); + return FileUtils.readFileToString(file, "utf-8"); + } + + private String buildUrl(StringBuilder pathToMock, JsonNode testSettings) { + pathToMock.append(this.applicationId); + + if (testSettings.get("Version") != null ) { + pathToMock.append(String.format("/versions/%s/predict", testSettings.get("Version").asText())); + } else { + pathToMock.append(String.format("/slots/%s/predict", testSettings.get("Slot").asText())); + } + pathToMock.append( + String.format( + "?verbose=%s&log=%s&show-all-intents=%s", + testSettings.get("IncludeInstanceData").asText(), + testSettings.get("Log").asText(), + testSettings.get("IncludeAllIntents").asText() + ) + ); + + return pathToMock.toString(); + } + + private HttpUrl initializeMockServer(MockWebServer mockWebServer, JsonNode v3Response, String url) throws IOException { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + String mockResponse = mapper.writeValueAsString(v3Response); + mockWebServer.enqueue(new MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .setBody(mockResponse)); + + mockWebServer.start(); + + return mockWebServer.url(url); + } + + private LuisRecognizerOptionsV3 buildTestRecognizer (String endpoint, JsonNode testSettings) throws IOException { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectReader readerDynamicList = mapper.readerFor(new TypeReference>() {}); + ObjectReader readerExternalentities = mapper.readerFor(new TypeReference>() {}); + LuisRecognizerOptionsV3 recognizer = new LuisRecognizerOptionsV3( + new LuisApplication( + this.applicationId, + this.subscriptionKey, + endpoint)); + recognizer.setIncludeInstanceData(testSettings.get("IncludeInstanceData").asBoolean()); + recognizer.setIncludeAllIntents(testSettings.get("IncludeAllIntents").asBoolean()); + recognizer.setVersion(testSettings.get("Version") == null ? null : testSettings.get("Version").asText()); + recognizer.setDynamicLists(testSettings.get("DynamicLists") == null ? null : readerDynamicList.readValue(testSettings.get("DynamicLists"))); + recognizer.setExternalEntities(testSettings.get("ExternalEntities") == null ? null : readerExternalentities.readValue(testSettings.get("ExternalEntities"))); + recognizer.setDateTimeReference(testSettings.get("DateTimeReference") == null ? null : testSettings.get("DateTimeReference").asText()); + return recognizer; + } + +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisRecognizerTests.java b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisRecognizerTests.java new file mode 100644 index 000000000..884247522 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/LuisRecognizerTests.java @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.luis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.microsoft.bot.ai.luis.testdata.TestRecognizerResultConvert; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.IntentScore; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class LuisRecognizerTests { + + @Mock + LuisRecognizerOptionsV3 options; + + @Mock + BotTelemetryClient telemetryClient; + + @Mock + TurnContext turnContext; + + @Mock + DialogContext dialogContext; + + @Mock + LuisApplication luisApplication; + + private RecognizerResult getMockedResult() { + RecognizerResult recognizerResult = new RecognizerResult(); + HashMap intents = new HashMap(); + IntentScore testScore = new IntentScore(); + testScore.setScore(0.2); + IntentScore greetingScore = new IntentScore(); + greetingScore.setScore(0.4); + intents.put("Test", testScore); + intents.put("Greeting", greetingScore); + recognizerResult.setIntents(intents); + recognizerResult.setEntities(JsonNodeFactory.instance.objectNode()); + recognizerResult.setProperties( + "sentiment", + JsonNodeFactory.instance.objectNode() + .put( + "label", + "neutral")); + return recognizerResult; + }; + + @Test + public void topIntentReturnsTopIntent() { + String greetingIntent = LuisRecognizer + .topIntent(getMockedResult()); + assertEquals(greetingIntent, "Greeting"); + } + + @Test + public void topIntentReturnsDefaultIfMinScoreIsHigher() { + String defaultIntent = LuisRecognizer + .topIntent(getMockedResult(), 0.5); + assertEquals(defaultIntent, "None"); + } + + @Test + public void topIntentReturnsDefaultIfProvided() { + String defaultIntent = LuisRecognizer + .topIntent(getMockedResult(), "Test2", 0.5); + assertEquals(defaultIntent, "Test2"); + } + + @Test + public void topIntentThrowsIllegalArgumentIfResultIsNull() { + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisRecognizer.topIntent(null); + }); + + String expectedMessage = "RecognizerResult"; + String actualMessage = exception.getMessage(); + + Assert.assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + public void TopIntentReturnsTopIntentIfScoreEqualsMinScore() { + String defaultIntent = LuisRecognizer.topIntent(getMockedResult(), 0.4); + assertEquals(defaultIntent, "Greeting"); + } + + @Test + public void throwExceptionOnNullOptions() { + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { + LuisRecognizer lR = new LuisRecognizer(null); + }); + + String actualMessage = exception.getMessage(); + Assert.assertTrue(actualMessage.contains("Recognizer Options cannot be null")); + } + + @Test + public void recognizerResult() { + setMockObjectsForTelemetry(); + LuisRecognizer recognizer = new LuisRecognizer(options); + RecognizerResult expected = new RecognizerResult(); + expected.setText("Random Message"); + HashMap intents = new HashMap(); + IntentScore testScore = new IntentScore(); + testScore.setScore(0.2); + IntentScore greetingScore = new IntentScore(); + greetingScore.setScore(0.4); + intents.put("Test", testScore); + intents.put("Greeting", greetingScore); + expected.setIntents(intents); + expected.setEntities(JsonNodeFactory.instance.objectNode()); + expected.setProperties( + "sentiment", + JsonNodeFactory.instance.objectNode() + .put( + "label", + "neutral")); + RecognizerResult actual = null; + try { + actual = recognizer.recognize(turnContext).get(); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + assertEquals(mapper.writeValueAsString(expected), mapper.writeValueAsString(actual)); + } catch (InterruptedException | ExecutionException | JsonProcessingException e) { + e.printStackTrace(); + Assert.assertTrue(false); + } + } + + @Test + public void recognizerResult_nullTelemetryClient() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Random Message"); + activity.setChannelId("EmptyContext"); + ChannelAccount channelAccount = new ChannelAccount(); + channelAccount.setId("Activity-from-ID"); + activity.setFrom(channelAccount); + when(turnContext.getActivity()) + .thenReturn(activity); + + when(luisApplication.getApplicationId()) + .thenReturn("b31aeaf3-3511-495b-a07f-571fc873214b"); + + when(options.getApplication()) + .thenReturn(luisApplication); + RecognizerResult mockedResult = getMockedResult(); + mockedResult.setText("Random Message"); + doReturn(CompletableFuture.supplyAsync(() -> mockedResult)) + .when(options) + .recognizeInternal( + any(TurnContext.class)); + + LuisRecognizer recognizer = new LuisRecognizer(options); + RecognizerResult expected = new RecognizerResult(); + expected.setText("Random Message"); + HashMap intents = new HashMap(); + IntentScore testScore = new IntentScore(); + testScore.setScore(0.2); + IntentScore greetingScore = new IntentScore(); + greetingScore.setScore(0.4); + intents.put("Test", testScore); + intents.put("Greeting", greetingScore); + expected.setIntents(intents); + expected.setEntities(JsonNodeFactory.instance.objectNode()); + expected.setProperties( + "sentiment", + JsonNodeFactory.instance.objectNode() + .put( + "label", + "neutral")); + RecognizerResult actual = null; + try { + actual = recognizer.recognize(turnContext).get(); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + assertEquals(mapper.writeValueAsString(expected), mapper.writeValueAsString(actual)); + } catch (InterruptedException | ExecutionException | JsonProcessingException e) { + e.printStackTrace(); + Assert.assertTrue(false); + } + } + + @Test + public void recognizerResultDialogContext() { + RecognizerResult expected = new RecognizerResult(); + expected.setText("Random Message"); + HashMap intents = new HashMap(); + IntentScore testScore = new IntentScore(); + testScore.setScore(0.2); + IntentScore greetingScore = new IntentScore(); + greetingScore.setScore(0.4); + intents.put("Test", testScore); + intents.put("Greeting", greetingScore); + expected.setIntents(intents); + expected.setEntities(JsonNodeFactory.instance.objectNode()); + expected.setProperties( + "sentiment", + JsonNodeFactory.instance.objectNode() + .put( + "label", + "neutral")); + RecognizerResult actual = null; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Random Message"); + activity.setChannelId("EmptyContext"); + ChannelAccount channelAccount = new ChannelAccount(); + channelAccount.setId("Activity-from-ID"); + activity.setFrom(channelAccount); + when(turnContext.getActivity()) + .thenReturn(activity); + when(luisApplication.getApplicationId()) + .thenReturn("b31aeaf3-3511-495b-a07f-571fc873214b"); + + when(options.getTelemetryClient()).thenReturn(telemetryClient); + + when(options.getApplication()) + .thenReturn(luisApplication); + RecognizerResult mockedResult = getMockedResult(); + mockedResult.setText("Random Message"); + when(dialogContext.getContext()) + .thenReturn(turnContext); + + doReturn(CompletableFuture.supplyAsync(() -> mockedResult)) + .when(options) + .recognizeInternal( + any(DialogContext.class), any(Activity.class)); + LuisRecognizer recognizer = new LuisRecognizer(options); + try { + actual = recognizer.recognize(dialogContext, turnContext.getActivity()).get(); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + assertEquals(mapper.writeValueAsString(expected), mapper.writeValueAsString(actual)); + } catch (InterruptedException | ExecutionException | JsonProcessingException e) { + e.printStackTrace(); + Assert.assertTrue(false); + } + } + + @Test + public void recognizerResultConverted() { + + setMockObjectsForTelemetry(); + LuisRecognizer recognizer = new LuisRecognizer(options); + TestRecognizerResultConvert actual = null; + try { + actual = recognizer.recognize(turnContext, TestRecognizerResultConvert.class).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + + TestRecognizerResultConvert expected = new TestRecognizerResultConvert(); + expected.recognizerResultText = "Random Message"; + + assertEquals(expected.recognizerResultText, actual.recognizerResultText); + } + + @Test + public void telemetryPropertiesAreFilledOnRecognizer() { + + setMockObjectsForTelemetry(); + LuisRecognizer recognizer = new LuisRecognizer(options); + + try { + recognizer.recognize(turnContext).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + Map expectedProperties = new HashMap(); + expectedProperties.put("intentScore", "0.4"); + expectedProperties.put("intent2", "Test"); + expectedProperties.put("entities", "{}"); + expectedProperties.put("intentScore2", "0.2"); + expectedProperties.put("applicationId", "b31aeaf3-3511-495b-a07f-571fc873214b"); + expectedProperties.put("intent", "Greeting"); + expectedProperties.put("fromId", "Activity-from-ID"); + expectedProperties.put("sentimentLabel", "neutral"); + + verify(telemetryClient, atLeastOnce()).trackEvent("LuisResult", expectedProperties, null); + } + + @Test + public void telemetry_PiiLogged() { + + setMockObjectsForTelemetry(); + when(options.isLogPersonalInformation()).thenReturn(true); + + LuisRecognizer recognizer = new LuisRecognizer(options); + + try { + recognizer.recognize(turnContext).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + Map expectedProperties = new HashMap(); + expectedProperties.put("intentScore", "0.4"); + expectedProperties.put("intent2", "Test"); + expectedProperties.put("entities", "{}"); + expectedProperties.put("intentScore2", "0.2"); + expectedProperties.put("applicationId", "b31aeaf3-3511-495b-a07f-571fc873214b"); + expectedProperties.put("intent", "Greeting"); + expectedProperties.put("fromId", "Activity-from-ID"); + expectedProperties.put("sentimentLabel", "neutral"); + expectedProperties.put("question", "Random Message"); + + verify(telemetryClient, atLeastOnce()).trackEvent("LuisResult", expectedProperties, null); + } + + @Test + public void telemetry_additionalProperties() { + setMockObjectsForTelemetry(); + when(options.isLogPersonalInformation()).thenReturn(true); + + LuisRecognizer recognizer = new LuisRecognizer(options); + Map additionalProperties = new HashMap(); + additionalProperties.put("test", "testvalue"); + additionalProperties.put("foo", "foovalue"); + Map telemetryMetrics = new HashMap(); + telemetryMetrics.put("test", 3.1416); + telemetryMetrics.put("foo", 2.11); + try { + recognizer.recognize(turnContext, additionalProperties, telemetryMetrics).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + Map expectedProperties = new HashMap(); + expectedProperties.put("intentScore", "0.4"); + expectedProperties.put("intent2", "Test"); + expectedProperties.put("entities", "{}"); + expectedProperties.put("intentScore2", "0.2"); + expectedProperties.put("applicationId", "b31aeaf3-3511-495b-a07f-571fc873214b"); + expectedProperties.put("intent", "Greeting"); + expectedProperties.put("fromId", "Activity-from-ID"); + expectedProperties.put("sentimentLabel", "neutral"); + expectedProperties.put("question", "Random Message"); + expectedProperties.put("test", "testvalue"); + expectedProperties.put("foo", "foovalue"); + + verify(telemetryClient, atLeastOnce()).trackEvent("LuisResult", expectedProperties, telemetryMetrics); + } + + @Test + public void telemetry_additionalPropertiesOverrideProperty() { + setMockObjectsForTelemetry(); + when(options.isLogPersonalInformation()).thenReturn(true); + + LuisRecognizer recognizer = new LuisRecognizer(options); + Map additionalProperties = new HashMap(); + additionalProperties.put("intentScore", "1.15"); + additionalProperties.put("foo", "foovalue"); + Map telemetryMetrics = new HashMap(); + telemetryMetrics.put("test", 3.1416); + telemetryMetrics.put("foo", 2.11); + try { + recognizer.recognize(turnContext, additionalProperties, telemetryMetrics).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + Map expectedProperties = new HashMap(); + expectedProperties.put("intentScore", "1.15"); + expectedProperties.put("intent2", "Test"); + expectedProperties.put("entities", "{}"); + expectedProperties.put("intentScore2", "0.2"); + expectedProperties.put("applicationId", "b31aeaf3-3511-495b-a07f-571fc873214b"); + expectedProperties.put("intent", "Greeting"); + expectedProperties.put("fromId", "Activity-from-ID"); + expectedProperties.put("sentimentLabel", "neutral"); + expectedProperties.put("question", "Random Message"); + expectedProperties.put("foo", "foovalue"); + + verify(telemetryClient, atLeastOnce()).trackEvent("LuisResult", expectedProperties, telemetryMetrics); + } + + private void setMockObjectsForTelemetry() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Random Message"); + activity.setType(ActivityTypes.MESSAGE); + activity.setChannelId("EmptyContext"); + ChannelAccount channelAccount = new ChannelAccount(); + channelAccount.setId("Activity-from-ID"); + activity.setFrom(channelAccount); + when(turnContext.getActivity()) + .thenReturn(activity); + + when(luisApplication.getApplicationId()) + .thenReturn("b31aeaf3-3511-495b-a07f-571fc873214b"); + + when(options.getTelemetryClient()).thenReturn(telemetryClient); + + when(options.getApplication()) + .thenReturn(luisApplication); + RecognizerResult mockedResult = getMockedResult(); + mockedResult.setText("Random Message"); + doReturn(CompletableFuture.supplyAsync(() -> mockedResult)) + .when(options) + .recognizeInternal( + any(TurnContext.class)); + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite1.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite1.json new file mode 100644 index 000000000..69ef1c20a --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite1.json @@ -0,0 +1,1971 @@ +{ + "entities": { + "$instance": { + "begin": [ + { + "endIndex": 12, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old", + "type": "builtin.age" + } + ], + "Composite1": [ + { + "endIndex": 306, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "type": "Composite1" + } + ], + "end": [ + { + "endIndex": 27, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days old", + "type": "builtin.age" + } + ], + "endpos": [ + { + "endIndex": 47, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 44, + "text": "3rd", + "type": "builtin.ordinalV2" + } + ], + "max": [ + { + "endIndex": 167, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 162, + "text": "$4.25", + "type": "builtin.currency" + } + ], + "ordinalV2": [ + { + "endIndex": 199, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 194, + "text": "first", + "type": "builtin.ordinalV2" + }, + { + "endIndex": 285, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 277, + "text": "next one", + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 306, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 294, + "text": "previous one", + "type": "builtin.ordinalV2.relative" + } + ] + }, + "begin": [ + { + "number": 12, + "units": "Year" + } + ], + "Composite1": [ + { + "$instance": { + "datetime": [ + { + "endIndex": 8, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years", + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 23, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days", + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 53, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 32, + "text": "monday july 3rd, 2019", + "type": "builtin.datetimeV2.date" + }, + { + "endIndex": 70, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 58, + "text": "every monday", + "type": "builtin.datetimeV2.set" + }, + { + "endIndex": 97, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 75, + "text": "between 3am and 5:30am", + "type": "builtin.datetimeV2.timerange" + } + ], + "dimension": [ + { + "endIndex": 109, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4 acres", + "type": "builtin.dimension" + }, + { + "endIndex": 127, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4 pico meters", + "type": "builtin.dimension" + } + ], + "email": [ + { + "endIndex": 150, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 132, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "money": [ + { + "endIndex": 157, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 155, + "text": "$4", + "type": "builtin.currency" + } + ], + "number": [ + { + "endIndex": 2, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12", + "type": "builtin.number" + }, + { + "endIndex": 18, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3", + "type": "builtin.number" + }, + { + "endIndex": 53, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "2019", + "type": "builtin.number" + }, + { + "endIndex": 92, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 91, + "text": "5", + "type": "builtin.number" + }, + { + "endIndex": 103, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4", + "type": "builtin.number" + }, + { + "endIndex": 115, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4", + "type": "builtin.number" + }, + { + "endIndex": 157, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "4", + "type": "builtin.number" + }, + { + "endIndex": 167, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 163, + "text": "4.25", + "type": "builtin.number" + }, + { + "endIndex": 179, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 177, + "text": "32", + "type": "builtin.number" + }, + { + "endIndex": 189, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 184, + "text": "210.4", + "type": "builtin.number" + }, + { + "endIndex": 206, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10", + "type": "builtin.number" + }, + { + "endIndex": 216, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5", + "type": "builtin.number" + }, + { + "endIndex": 225, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 222, + "text": "425", + "type": "builtin.number" + }, + { + "endIndex": 229, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "555", + "type": "builtin.number" + }, + { + "endIndex": 234, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 230, + "text": "1234", + "type": "builtin.number" + }, + { + "endIndex": 240, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3", + "type": "builtin.number" + }, + { + "endIndex": 258, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5", + "type": "builtin.number" + }, + { + "endIndex": 285, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 282, + "text": "one", + "type": "builtin.number" + }, + { + "endIndex": 306, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 303, + "text": "one", + "type": "builtin.number" + } + ], + "percentage": [ + { + "endIndex": 207, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10%", + "type": "builtin.percentage" + }, + { + "endIndex": 217, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5%", + "type": "builtin.percentage" + } + ], + "phonenumber": [ + { + "endIndex": 234, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 222, + "text": "425-555-1234", + "type": "builtin.phonenumber" + } + ], + "temperature": [ + { + "endIndex": 248, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3 degrees", + "type": "builtin.temperature" + }, + { + "endIndex": 268, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5 degrees c", + "type": "builtin.temperature" + } + ] + }, + "datetime": [ + { + "timex": [ + "P12Y" + ], + "type": "duration" + }, + { + "timex": [ + "P3D" + ], + "type": "duration" + }, + { + "timex": [ + "2019-07-03" + ], + "type": "date" + }, + { + "timex": [ + "XXXX-WXX-1" + ], + "type": "set" + }, + { + "timex": [ + "(T03,T05:30,PT2H30M)" + ], + "type": "timerange" + } + ], + "dimension": [ + { + "number": 4, + "units": "Acre" + }, + { + "number": 4, + "units": "Picometer" + } + ], + "email": [ + "chrimc@hotmail.com" + ], + "money": [ + { + "number": 4, + "units": "Dollar" + } + ], + "number": [ + 12, + 3, + 2019, + 5, + 4, + 4, + 4, + 4.25, + 32, + 210.4, + 10, + 10.5, + 425, + 555, + 1234, + 3, + -27.5, + 1, + 1 + ], + "percentage": [ + 10, + 10.5 + ], + "phonenumber": [ + "425-555-1234" + ], + "temperature": [ + { + "number": 3, + "units": "Degree" + }, + { + "number": -27.5, + "units": "C" + } + ] + } + ], + "end": [ + { + "number": 3, + "units": "Day" + } + ], + "endpos": [ + { + "offset": 3, + "relativeTo": "start" + } + ], + "max": [ + { + "number": 4.25, + "units": "Dollar" + } + ], + "ordinalV2": [ + { + "offset": 1, + "relativeTo": "start" + }, + { + "offset": 1, + "relativeTo": "current" + }, + { + "offset": -1, + "relativeTo": "current" + } + ] + }, + "intents": { + "Cancel": { + "score": 1.54311692E-06 + }, + "Delivery": { + "score": 0.000280677923 + }, + "EntityTests": { + "score": 0.958614767 + }, + "Greeting": { + "score": 8.076372E-07 + }, + "Help": { + "score": 4.74059061E-06 + }, + "None": { + "score": 0.0101076821 + }, + "Roles": { + "score": 0.191202149 + }, + "search": { + "score": 0.00475360872 + }, + "SpecifyName": { + "score": 7.367716E-05 + }, + "Travel": { + "score": 0.00232480234 + }, + "Weather_GetForecast": { + "score": 0.0141556319 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "compositeEntities": [ + { + "children": [ + { + "type": "builtin.datetimeV2.duration", + "value": "12 years" + }, + { + "type": "builtin.datetimeV2.duration", + "value": "3 days" + }, + { + "type": "builtin.datetimeV2.date", + "value": "monday july 3rd, 2019" + }, + { + "type": "builtin.datetimeV2.set", + "value": "every monday" + }, + { + "type": "builtin.datetimeV2.timerange", + "value": "between 3am and 5:30am" + }, + { + "type": "builtin.dimension", + "value": "4 acres" + }, + { + "type": "builtin.dimension", + "value": "4 pico meters" + }, + { + "type": "builtin.email", + "value": "chrimc@hotmail.com" + }, + { + "type": "builtin.currency", + "value": "$4" + }, + { + "type": "builtin.number", + "value": "12" + }, + { + "type": "builtin.number", + "value": "3" + }, + { + "type": "builtin.number", + "value": "2019" + }, + { + "type": "builtin.number", + "value": "5" + }, + { + "type": "builtin.number", + "value": "4" + }, + { + "type": "builtin.number", + "value": "4" + }, + { + "type": "builtin.number", + "value": "4" + }, + { + "type": "builtin.number", + "value": "4.25" + }, + { + "type": "builtin.number", + "value": "32" + }, + { + "type": "builtin.number", + "value": "210.4" + }, + { + "type": "builtin.number", + "value": "10" + }, + { + "type": "builtin.number", + "value": "10.5" + }, + { + "type": "builtin.number", + "value": "425" + }, + { + "type": "builtin.number", + "value": "555" + }, + { + "type": "builtin.number", + "value": "1234" + }, + { + "type": "builtin.number", + "value": "3" + }, + { + "type": "builtin.number", + "value": "-27.5" + }, + { + "type": "builtin.number", + "value": "one" + }, + { + "type": "builtin.number", + "value": "one" + }, + { + "type": "builtin.percentage", + "value": "10%" + }, + { + "type": "builtin.percentage", + "value": "10.5%" + }, + { + "type": "builtin.phonenumber", + "value": "425-555-1234" + }, + { + "type": "builtin.temperature", + "value": "3 degrees" + }, + { + "type": "builtin.temperature", + "value": "-27.5 degrees c" + } + ], + "parentType": "Composite1", + "value": "12 years old and 3 days old and monday july 3rd , 2019 and every monday and between 3am and 5 : 30am and 4 acres and 4 pico meters and chrimc @ hotmail . com and $ 4 and $ 4 . 25 and also 32 and 210 . 4 and first and 10 % and 10 . 5 % and 425 - 555 - 1234 and 3 degrees and - 27 . 5 degrees c and the next one and the previous one" + } + ], + "entities": [ + { + "endIndex": 305, + "entity": "12 years old and 3 days old and monday july 3rd , 2019 and every monday and between 3am and 5 : 30am and 4 acres and 4 pico meters and chrimc @ hotmail . com and $ 4 and $ 4 . 25 and also 32 and 210 . 4 and first and 10 % and 10 . 5 % and 425 - 555 - 1234 and 3 degrees and - 27 . 5 degrees c and the next one and the previous one", + "score": 0.9074669, + "startIndex": 0, + "type": "Composite1" + }, + { + "endIndex": 1, + "entity": "12", + "resolution": { + "subtype": "integer", + "value": "12" + }, + "startIndex": 0, + "type": "builtin.number" + }, + { + "endIndex": 17, + "entity": "3", + "resolution": { + "subtype": "integer", + "value": "3" + }, + "startIndex": 17, + "type": "builtin.number" + }, + { + "endIndex": 52, + "entity": "2019", + "resolution": { + "subtype": "integer", + "value": "2019" + }, + "startIndex": 49, + "type": "builtin.number" + }, + { + "endIndex": 91, + "entity": "5", + "resolution": { + "subtype": "integer", + "value": "5" + }, + "startIndex": 91, + "type": "builtin.number" + }, + { + "endIndex": 102, + "entity": "4", + "resolution": { + "subtype": "integer", + "value": "4" + }, + "startIndex": 102, + "type": "builtin.number" + }, + { + "endIndex": 114, + "entity": "4", + "resolution": { + "subtype": "integer", + "value": "4" + }, + "startIndex": 114, + "type": "builtin.number" + }, + { + "endIndex": 156, + "entity": "4", + "resolution": { + "subtype": "integer", + "value": "4" + }, + "startIndex": 156, + "type": "builtin.number" + }, + { + "endIndex": 166, + "entity": "4.25", + "resolution": { + "subtype": "decimal", + "value": "4.25" + }, + "startIndex": 163, + "type": "builtin.number" + }, + { + "endIndex": 178, + "entity": "32", + "resolution": { + "subtype": "integer", + "value": "32" + }, + "startIndex": 177, + "type": "builtin.number" + }, + { + "endIndex": 188, + "entity": "210.4", + "resolution": { + "subtype": "decimal", + "value": "210.4" + }, + "startIndex": 184, + "type": "builtin.number" + }, + { + "endIndex": 205, + "entity": "10", + "resolution": { + "subtype": "integer", + "value": "10" + }, + "startIndex": 204, + "type": "builtin.number" + }, + { + "endIndex": 215, + "entity": "10.5", + "resolution": { + "subtype": "decimal", + "value": "10.5" + }, + "startIndex": 212, + "type": "builtin.number" + }, + { + "endIndex": 224, + "entity": "425", + "resolution": { + "subtype": "integer", + "value": "425" + }, + "startIndex": 222, + "type": "builtin.number" + }, + { + "endIndex": 228, + "entity": "555", + "resolution": { + "subtype": "integer", + "value": "555" + }, + "startIndex": 226, + "type": "builtin.number" + }, + { + "endIndex": 233, + "entity": "1234", + "resolution": { + "subtype": "integer", + "value": "1234" + }, + "startIndex": 230, + "type": "builtin.number" + }, + { + "endIndex": 239, + "entity": "3", + "resolution": { + "subtype": "integer", + "value": "3" + }, + "startIndex": 239, + "type": "builtin.number" + }, + { + "endIndex": 257, + "entity": "-27.5", + "resolution": { + "subtype": "decimal", + "value": "-27.5" + }, + "startIndex": 253, + "type": "builtin.number" + }, + { + "endIndex": 284, + "entity": "one", + "resolution": { + "subtype": "integer", + "value": "1" + }, + "startIndex": 282, + "type": "builtin.number" + }, + { + "endIndex": 305, + "entity": "one", + "resolution": { + "subtype": "integer", + "value": "1" + }, + "startIndex": 303, + "type": "builtin.number" + }, + { + "endIndex": 11, + "entity": "12 years old", + "resolution": { + "unit": "Year", + "value": "12" + }, + "role": "begin", + "startIndex": 0, + "type": "builtin.age" + }, + { + "endIndex": 26, + "entity": "3 days old", + "resolution": { + "unit": "Day", + "value": "3" + }, + "role": "end", + "startIndex": 17, + "type": "builtin.age" + }, + { + "endIndex": 7, + "entity": "12 years", + "resolution": { + "values": [ + { + "timex": "P12Y", + "type": "duration", + "value": "378432000" + } + ] + }, + "startIndex": 0, + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 22, + "entity": "3 days", + "resolution": { + "values": [ + { + "timex": "P3D", + "type": "duration", + "value": "259200" + } + ] + }, + "startIndex": 17, + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 52, + "entity": "monday july 3rd, 2019", + "resolution": { + "values": [ + { + "timex": "2019-07-03", + "type": "date", + "value": "2019-07-03" + } + ] + }, + "startIndex": 32, + "type": "builtin.datetimeV2.date" + }, + { + "endIndex": 69, + "entity": "every monday", + "resolution": { + "values": [ + { + "timex": "XXXX-WXX-1", + "type": "set", + "value": "not resolved" + } + ] + }, + "startIndex": 58, + "type": "builtin.datetimeV2.set" + }, + { + "endIndex": 96, + "entity": "between 3am and 5:30am", + "resolution": { + "values": [ + { + "end": "05:30:00", + "start": "03:00:00", + "timex": "(T03,T05:30,PT2H30M)", + "type": "timerange" + } + ] + }, + "startIndex": 75, + "type": "builtin.datetimeV2.timerange" + }, + { + "endIndex": 108, + "entity": "4 acres", + "resolution": { + "unit": "Acre", + "value": "4" + }, + "startIndex": 102, + "type": "builtin.dimension" + }, + { + "endIndex": 126, + "entity": "4 pico meters", + "resolution": { + "unit": "Picometer", + "value": "4" + }, + "startIndex": 114, + "type": "builtin.dimension" + }, + { + "endIndex": 149, + "entity": "chrimc@hotmail.com", + "resolution": { + "value": "chrimc@hotmail.com" + }, + "startIndex": 132, + "type": "builtin.email" + }, + { + "endIndex": 156, + "entity": "$4", + "resolution": { + "unit": "Dollar", + "value": "4" + }, + "startIndex": 155, + "type": "builtin.currency" + }, + { + "endIndex": 166, + "entity": "$4.25", + "resolution": { + "unit": "Dollar", + "value": "4.25" + }, + "role": "max", + "startIndex": 162, + "type": "builtin.currency" + }, + { + "endIndex": 46, + "entity": "3rd", + "resolution": { + "offset": "3", + "relativeTo": "start" + }, + "role": "endpos", + "startIndex": 44, + "type": "builtin.ordinalV2" + }, + { + "endIndex": 198, + "entity": "first", + "resolution": { + "offset": "1", + "relativeTo": "start" + }, + "startIndex": 194, + "type": "builtin.ordinalV2" + }, + { + "endIndex": 284, + "entity": "next one", + "resolution": { + "offset": "1", + "relativeTo": "current" + }, + "startIndex": 277, + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 305, + "entity": "previous one", + "resolution": { + "offset": "-1", + "relativeTo": "current" + }, + "startIndex": 294, + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 206, + "entity": "10%", + "resolution": { + "value": "10%" + }, + "startIndex": 204, + "type": "builtin.percentage" + }, + { + "endIndex": 216, + "entity": "10.5%", + "resolution": { + "value": "10.5%" + }, + "startIndex": 212, + "type": "builtin.percentage" + }, + { + "endIndex": 233, + "entity": "425-555-1234", + "resolution": { + "score": "0.9", + "value": "425-555-1234" + }, + "startIndex": 222, + "type": "builtin.phonenumber" + }, + { + "endIndex": 247, + "entity": "3 degrees", + "resolution": { + "unit": "Degree", + "value": "3" + }, + "startIndex": 239, + "type": "builtin.temperature" + }, + { + "endIndex": 267, + "entity": "-27.5 degrees c", + "resolution": { + "unit": "C", + "value": "-27.5" + }, + "startIndex": 253, + "type": "builtin.temperature" + } + ], + "intents": [ + { + "intent": "EntityTests", + "score": 0.958614767 + }, + { + "intent": "Roles", + "score": 0.191202149 + }, + { + "intent": "Weather.GetForecast", + "score": 0.0141556319 + }, + { + "intent": "None", + "score": 0.0101076821 + }, + { + "intent": "search", + "score": 0.00475360872 + }, + { + "intent": "Travel", + "score": 0.00232480234 + }, + { + "intent": "Delivery", + "score": 0.000280677923 + }, + { + "intent": "SpecifyName", + "score": 7.367716E-05 + }, + { + "intent": "Help", + "score": 4.74059061E-06 + }, + { + "intent": "Cancel", + "score": 1.54311692E-06 + }, + { + "intent": "Greeting", + "score": 8.076372E-07 + } + ], + "query": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "EntityTests", + "score": 0.958614767 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "begin": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "begin", + "startIndex": 0, + "text": "12 years old", + "type": "builtin.age" + } + ], + "Composite1": [ + { + "length": 306, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "type": "Composite1" + } + ], + "end": [ + { + "length": 10, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "end", + "startIndex": 17, + "text": "3 days old", + "type": "builtin.age" + } + ], + "endpos": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "endpos", + "startIndex": 44, + "text": "3rd", + "type": "builtin.ordinalV2" + } + ], + "max": [ + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "max", + "startIndex": 162, + "text": "$4.25", + "type": "builtin.currency" + } + ], + "ordinalV2": [ + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 194, + "text": "first", + "type": "builtin.ordinalV2" + }, + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 277, + "text": "next one", + "type": "builtin.ordinalV2.relative" + }, + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 294, + "text": "previous one", + "type": "builtin.ordinalV2.relative" + } + ] + }, + "begin": [ + { + "number": 12, + "units": "Year" + } + ], + "Composite1": [ + { + "$instance": { + "datetimeV2": [ + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 6, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 21, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 32, + "text": "monday july 3rd, 2019", + "type": "builtin.datetimeV2.date" + }, + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 58, + "text": "every monday", + "type": "builtin.datetimeV2.set" + }, + { + "length": 22, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 75, + "text": "between 3am and 5:30am", + "type": "builtin.datetimeV2.timerange" + } + ], + "dimension": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4 acres", + "type": "builtin.dimension" + }, + { + "length": 13, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4 pico meters", + "type": "builtin.dimension" + } + ], + "email": [ + { + "length": 18, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 132, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "money": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 155, + "text": "$4", + "type": "builtin.currency" + } + ], + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "2019", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 91, + "text": "5", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "4", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 163, + "text": "4.25", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 177, + "text": "32", + "type": "builtin.number" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 184, + "text": "210.4", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 222, + "text": "425", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "555", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 230, + "text": "1234", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3", + "type": "builtin.number" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 282, + "text": "one", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 303, + "text": "one", + "type": "builtin.number" + } + ], + "percentage": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10%", + "type": "builtin.percentage" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5%", + "type": "builtin.percentage" + } + ], + "phonenumber": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 222, + "text": "425-555-1234", + "type": "builtin.phonenumber" + } + ], + "temperature": [ + { + "length": 9, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3 degrees", + "type": "builtin.temperature" + }, + { + "length": 15, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5 degrees c", + "type": "builtin.temperature" + } + ] + }, + "datetimeV2": [ + { + "type": "duration", + "values": [ + { + "resolution": [ + { + "value": "378432000" + } + ], + "timex": "P12Y" + } + ] + }, + { + "type": "duration", + "values": [ + { + "resolution": [ + { + "value": "259200" + } + ], + "timex": "P3D" + } + ] + }, + { + "type": "date", + "values": [ + { + "resolution": [ + { + "value": "2019-07-03" + } + ], + "timex": "2019-07-03" + } + ] + }, + { + "type": "set", + "values": [ + { + "resolution": [ + { + "value": "not resolved" + } + ], + "timex": "XXXX-WXX-1" + } + ] + }, + { + "type": "timerange", + "values": [ + { + "resolution": [ + { + "end": "05:30:00", + "start": "03:00:00" + } + ], + "timex": "(T03,T05:30,PT2H30M)" + } + ] + } + ], + "dimension": [ + { + "number": 4, + "units": "Acre" + }, + { + "number": 4, + "units": "Picometer" + } + ], + "email": [ + "chrimc@hotmail.com" + ], + "money": [ + { + "number": 4, + "units": "Dollar" + } + ], + "number": [ + 12, + 3, + 2019, + 5, + 4, + 4, + 4, + 4.25, + 32, + 210.4, + 10, + 10.5, + 425, + 555, + 1234, + 3, + -27.5, + 1, + 1 + ], + "percentage": [ + 10, + 10.5 + ], + "phonenumber": [ + "425-555-1234" + ], + "temperature": [ + { + "number": 3, + "units": "Degree" + }, + { + "number": -27.5, + "units": "C" + } + ] + } + ], + "end": [ + { + "number": 3, + "units": "Day" + } + ], + "endpos": [ + { + "offset": 3, + "relativeTo": "start" + } + ], + "max": [ + { + "number": 4.25, + "units": "Dollar" + } + ], + "ordinalV2": [ + { + "offset": 1, + "relativeTo": "start" + }, + { + "offset": 1, + "relativeTo": "current" + }, + { + "offset": -1, + "relativeTo": "current" + } + ] + }, + "intents": { + "Cancel": { + "score": 1.54311692E-06 + }, + "Delivery": { + "score": 0.000280677923 + }, + "EntityTests": { + "score": 0.958614767 + }, + "Greeting": { + "score": 8.076372E-07 + }, + "Help": { + "score": 4.74059061E-06 + }, + "None": { + "score": 0.0101076821 + }, + "Roles": { + "score": 0.191202149 + }, + "search": { + "score": 0.00475360872 + }, + "SpecifyName": { + "score": 7.367716E-05 + }, + "Travel": { + "score": 0.00232480234 + }, + "Weather.GetForecast": { + "score": 0.0141556319 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite2.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite2.json new file mode 100644 index 000000000..5d79c500f --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite2.json @@ -0,0 +1,435 @@ +{ + "entities": { + "$instance": { + "Composite2": [ + { + "endIndex": 69, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com is where you can fly from seattle to dallas via denver", + "type": "Composite2" + } + ], + "geographyV2": [ + { + "endIndex": 48, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "seattle", + "type": "builtin.geographyV2.city" + } + ], + "oldURL": [ + { + "endIndex": 14, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com", + "type": "builtin.url" + } + ] + }, + "Composite2": [ + { + "$instance": { + "City": [ + { + "endIndex": 69, + "modelType": "Hierarchical Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 63, + "text": "denver", + "type": "City" + } + ], + "From": [ + { + "endIndex": 48, + "modelType": "Hierarchical Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "seattle", + "type": "City::From" + } + ], + "To": [ + { + "endIndex": 58, + "modelType": "Hierarchical Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 52, + "text": "dallas", + "type": "City::To" + } + ] + }, + "City": [ + "denver" + ], + "From": [ + "seattle" + ], + "To": [ + "dallas" + ] + } + ], + "geographyV2": [ + { + "location": "seattle", + "type": "city" + } + ], + "oldURL": [ + "http://foo.com" + ] + }, + "intents": { + "Cancel": { + "score": 0.000219483933 + }, + "Delivery": { + "score": 0.00125586381 + }, + "EntityTests": { + "score": 0.956510365 + }, + "Greeting": { + "score": 0.00014909108 + }, + "Help": { + "score": 0.0005319686 + }, + "None": { + "score": 0.003814332 + }, + "Roles": { + "score": 0.02785043 + }, + "search": { + "score": 0.00132194813 + }, + "SpecifyName": { + "score": 0.000922683743 + }, + "Travel": { + "score": 0.01013992 + }, + "Weather_GetForecast": { + "score": 0.0228957664 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "http://foo.com is where you can fly from seattle to dallas via denver", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "compositeEntities": [ + { + "children": [ + { + "type": "City", + "value": "denver" + }, + { + "type": "City::From", + "value": "seattle" + }, + { + "type": "City::To", + "value": "dallas" + } + ], + "parentType": "Composite2", + "value": "http : / / foo . com is where you can fly from seattle to dallas via denver" + } + ], + "entities": [ + { + "endIndex": 47, + "entity": "seattle", + "score": 0.997107, + "startIndex": 41, + "type": "City::From" + }, + { + "endIndex": 57, + "entity": "dallas", + "score": 0.998217642, + "startIndex": 52, + "type": "City::To" + }, + { + "endIndex": 68, + "entity": "denver", + "score": 0.991177261, + "startIndex": 63, + "type": "City" + }, + { + "endIndex": 68, + "entity": "http : / / foo . com is where you can fly from seattle to dallas via denver", + "score": 0.9807907, + "startIndex": 0, + "type": "Composite2" + }, + { + "endIndex": 47, + "entity": "seattle", + "startIndex": 41, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 13, + "entity": "http://foo.com", + "resolution": { + "value": "http://foo.com" + }, + "role": "oldURL", + "startIndex": 0, + "type": "builtin.url" + } + ], + "intents": [ + { + "intent": "EntityTests", + "score": 0.956510365 + }, + { + "intent": "Roles", + "score": 0.02785043 + }, + { + "intent": "Weather.GetForecast", + "score": 0.0228957664 + }, + { + "intent": "Travel", + "score": 0.01013992 + }, + { + "intent": "None", + "score": 0.003814332 + }, + { + "intent": "search", + "score": 0.00132194813 + }, + { + "intent": "Delivery", + "score": 0.00125586381 + }, + { + "intent": "SpecifyName", + "score": 0.000922683743 + }, + { + "intent": "Help", + "score": 0.0005319686 + }, + { + "intent": "Cancel", + "score": 0.000219483933 + }, + { + "intent": "Greeting", + "score": 0.00014909108 + } + ], + "query": "http://foo.com is where you can fly from seattle to dallas via denver", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "EntityTests", + "score": 0.956510365 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Composite2": [ + { + "length": 69, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com is where you can fly from seattle to dallas via denver", + "type": "Composite2" + } + ], + "geographyV2": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "seattle", + "type": "builtin.geographyV2.city" + } + ], + "oldURL": [ + { + "length": 14, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "oldURL", + "startIndex": 0, + "text": "http://foo.com", + "type": "builtin.url" + } + ] + }, + "Composite2": [ + { + "$instance": { + "City": [ + { + "length": 6, + "modelType": "Hierarchical Entity Extractor", + "modelTypeId": 3, + "recognitionSources": [ + "model" + ], + "startIndex": 63, + "text": "denver", + "type": "City" + } + ], + "City::From": [ + { + "length": 7, + "modelType": "Hierarchical Entity Extractor", + "modelTypeId": 3, + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "seattle", + "type": "City::From" + } + ], + "City::To": [ + { + "length": 6, + "modelType": "Hierarchical Entity Extractor", + "modelTypeId": 3, + "recognitionSources": [ + "model" + ], + "startIndex": 52, + "text": "dallas", + "type": "City::To" + } + ] + }, + "City": [ + "denver" + ], + "City::From": [ + "seattle" + ], + "City::To": [ + "dallas" + ] + } + ], + "geographyV2": [ + { + "type": "city", + "value": "seattle" + } + ], + "oldURL": [ + "http://foo.com" + ] + }, + "intents": { + "Cancel": { + "score": 0.000219483933 + }, + "Delivery": { + "score": 0.00125586381 + }, + "EntityTests": { + "score": 0.956510365 + }, + "Greeting": { + "score": 0.00014909108 + }, + "Help": { + "score": 0.0005319686 + }, + "None": { + "score": 0.003814332 + }, + "Roles": { + "score": 0.02785043 + }, + "search": { + "score": 0.00132194813 + }, + "SpecifyName": { + "score": 0.000922683743 + }, + "Travel": { + "score": 0.01013992 + }, + "Weather.GetForecast": { + "score": 0.0228957664 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "http://foo.com is where you can fly from seattle to dallas via denver" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite3.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite3.json new file mode 100644 index 000000000..863187133 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Composite3.json @@ -0,0 +1,461 @@ +{ + "entities": { + "$instance": { + "Destination": [ + { + "endIndex": 33, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9818366, + "startIndex": 25, + "text": "12346 WA", + "type": "Address" + } + ], + "Source": [ + { + "endIndex": 21, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9345161, + "startIndex": 13, + "text": "12345 VA", + "type": "Address" + } + ] + }, + "Destination": [ + { + "$instance": { + "number": [ + { + "endIndex": 30, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 25, + "text": "12346", + "type": "builtin.number" + } + ], + "State": [ + { + "endIndex": 33, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9893861, + "startIndex": 31, + "text": "WA", + "type": "State" + } + ] + }, + "number": [ + 12346 + ], + "State": [ + "WA" + ] + } + ], + "Source": [ + { + "$instance": { + "number": [ + { + "endIndex": 18, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 13, + "text": "12345", + "type": "builtin.number" + } + ], + "State": [ + { + "endIndex": 21, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.941649556, + "startIndex": 19, + "text": "VA", + "type": "State" + } + ] + }, + "number": [ + 12345 + ], + "State": [ + "VA" + ] + } + ] + }, + "intents": { + "Cancel": { + "score": 1.01764708E-09 + }, + "Delivery": { + "score": 0.00238572317 + }, + "EntityTests": { + "score": 4.757576E-10 + }, + "Greeting": { + "score": 1.0875E-09 + }, + "Help": { + "score": 1.01764708E-09 + }, + "None": { + "score": 1.17844979E-06 + }, + "Roles": { + "score": 0.999911964 + }, + "search": { + "score": 9.494859E-06 + }, + "SpecifyName": { + "score": 3.0666667E-09 + }, + "Travel": { + "score": 3.09763345E-06 + }, + "Weather_GetForecast": { + "score": 1.02792524E-06 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "Deliver from 12345 VA to 12346 WA", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "compositeEntities": [ + { + "children": [ + { + "type": "builtin.number", + "value": "12345" + }, + { + "type": "State", + "value": "va" + } + ], + "parentType": "Address", + "value": "12345 va" + }, + { + "children": [ + { + "type": "builtin.number", + "value": "12346" + }, + { + "type": "State", + "value": "wa" + } + ], + "parentType": "Address", + "value": "12346 wa" + } + ], + "entities": [ + { + "endIndex": 20, + "entity": "va", + "score": 0.9684971, + "startIndex": 19, + "type": "State" + }, + { + "endIndex": 32, + "entity": "wa", + "score": 0.988121331, + "startIndex": 31, + "type": "State" + }, + { + "endIndex": 20, + "entity": "12345 va", + "role": "Source", + "score": 0.9659546, + "startIndex": 13, + "type": "Address" + }, + { + "endIndex": 32, + "entity": "12346 wa", + "role": "Destination", + "score": 0.987832844, + "startIndex": 25, + "type": "Address" + }, + { + "endIndex": 17, + "entity": "12345", + "resolution": { + "subtype": "integer", + "value": "12345" + }, + "startIndex": 13, + "type": "builtin.number" + }, + { + "endIndex": 29, + "entity": "12346", + "resolution": { + "subtype": "integer", + "value": "12346" + }, + "startIndex": 25, + "type": "builtin.number" + } + ], + "intents": [ + { + "intent": "Roles", + "score": 0.99991256 + }, + { + "intent": "Delivery", + "score": 0.00239894539 + }, + { + "intent": "None", + "score": 1.18518381E-06 + }, + { + "intent": "Weather.GetForecast", + "score": 1.03386708E-06 + }, + { + "intent": "search", + "score": 9.45E-09 + }, + { + "intent": "SpecifyName", + "score": 3.08333337E-09 + }, + { + "intent": "Travel", + "score": 3.08333337E-09 + }, + { + "intent": "Greeting", + "score": 1.09375E-09 + }, + { + "intent": "Cancel", + "score": 1.02352937E-09 + }, + { + "intent": "Help", + "score": 1.02352937E-09 + }, + { + "intent": "EntityTests", + "score": 4.617647E-10 + } + ], + "query": "Deliver from 12345 VA to 12346 WA", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "Roles", + "score": 0.99991256 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production", + "Version": "GeoPeople" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Destination": [ + { + "length": 8, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "role": "Destination", + "score": 0.9818366, + "startIndex": 25, + "text": "12346 WA", + "type": "Address" + } + ], + "Source": [ + { + "length": 8, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "role": "Source", + "score": 0.9345161, + "startIndex": 13, + "text": "12345 VA", + "type": "Address" + } + ] + }, + "Destination": [ + { + "$instance": { + "number": [ + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 25, + "text": "12346", + "type": "builtin.number" + } + ], + "State": [ + { + "length": 2, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "score": 0.9893861, + "startIndex": 31, + "text": "WA", + "type": "State" + } + ] + }, + "number": [ + 12346 + ], + "State": [ + "WA" + ] + } + ], + "Source": [ + { + "$instance": { + "number": [ + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 13, + "text": "12345", + "type": "builtin.number" + } + ], + "State": [ + { + "length": 2, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "score": 0.941649556, + "startIndex": 19, + "text": "VA", + "type": "State" + } + ] + }, + "number": [ + 12345 + ], + "State": [ + "VA" + ] + } + ] + }, + "intents": { + "Cancel": { + "score": 1.01764708E-09 + }, + "Delivery": { + "score": 0.00238572317 + }, + "EntityTests": { + "score": 4.757576E-10 + }, + "Greeting": { + "score": 1.0875E-09 + }, + "Help": { + "score": 1.01764708E-09 + }, + "None": { + "score": 1.17844979E-06 + }, + "Roles": { + "score": 0.999911964 + }, + "search": { + "score": 9.494859E-06 + }, + "SpecifyName": { + "score": 3.0666667E-09 + }, + "Travel": { + "score": 3.09763345E-06 + }, + "Weather.GetForecast": { + "score": 1.02792524E-06 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Roles" + }, + "query": "Deliver from 12345 VA to 12346 WA" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Contoso App.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Contoso App.json new file mode 100644 index 000000000..330e757bd --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Contoso App.json @@ -0,0 +1,2541 @@ +{ + "luis_schema_version": "7.0.0", + "intents": [ + { + "name": "Cancel", + "features": [] + }, + { + "name": "Delivery", + "features": [] + }, + { + "name": "EntityTests", + "features": [] + }, + { + "name": "Greeting", + "features": [] + }, + { + "name": "Help", + "features": [] + }, + { + "name": "None", + "features": [] + }, + { + "name": "Roles", + "features": [] + }, + { + "name": "search", + "features": [] + }, + { + "name": "SpecifyName", + "features": [] + }, + { + "name": "Travel", + "features": [] + }, + { + "name": "Weather.GetForecast", + "features": [] + } + ], + "entities": [ + { + "name": "Name", + "children": [], + "roles": [ + "liker", + "likee" + ], + "features": [] + }, + { + "name": "State", + "children": [], + "roles": [], + "features": [] + }, + { + "name": "Weather.Location", + "children": [], + "roles": [ + "source", + "destination" + ], + "features": [] + } + ], + "hierarchicals": [ + { + "name": "City", + "children": [ + { + "name": "To" + }, + { + "name": "From" + } + ], + "roles": [], + "features": [] + } + ], + "composites": [ + { + "name": "Address", + "children": [ + { + "name": "number" + }, + { + "name": "State" + } + ], + "roles": [ + "Source", + "Destination" + ], + "features": [] + }, + { + "name": "Composite1", + "children": [ + { + "name": "age" + }, + { + "name": "datetimeV2" + }, + { + "name": "dimension" + }, + { + "name": "email" + }, + { + "name": "money" + }, + { + "name": "number" + }, + { + "name": "percentage" + }, + { + "name": "phonenumber" + }, + { + "name": "temperature" + } + ], + "roles": [], + "features": [] + }, + { + "name": "Composite2", + "children": [ + { + "name": "Airline" + }, + { + "name": "City" + }, + { + "name": "url" + }, + { + "name": "City::From" + }, + { + "name": "City::To" + }, + { + "name": "Weather.Location" + } + ], + "roles": [], + "features": [] + } + ], + "closedLists": [ + { + "name": "Airline", + "subLists": [ + { + "canonicalForm": "Delta", + "list": [ + "DL" + ] + }, + { + "canonicalForm": "Alaska", + "list": [] + }, + { + "canonicalForm": "United", + "list": [] + }, + { + "canonicalForm": "Virgin", + "list": [ + "DL" + ] + } + ], + "roles": [ + "Buyer", + "Seller" + ] + } + ], + "prebuiltEntities": [ + { + "name": "age", + "roles": [ + "end", + "begin" + ] + }, + { + "name": "datetimeV2", + "roles": [ + "leave", + "arrive" + ] + }, + { + "name": "dimension", + "roles": [ + "length", + "width" + ] + }, + { + "name": "email", + "roles": [ + "sender", + "receiver" + ] + }, + { + "name": "geographyV2", + "roles": [ + "endloc", + "startloc" + ] + }, + { + "name": "money", + "roles": [ + "max", + "min" + ] + }, + { + "name": "number", + "roles": [] + }, + { + "name": "ordinalV2", + "roles": [ + "endpos", + "startpos" + ] + }, + { + "name": "percentage", + "roles": [ + "maximum", + "minimum" + ] + }, + { + "name": "personName", + "roles": [ + "child", + "parent" + ] + }, + { + "name": "phonenumber", + "roles": [ + "old", + "newPhone" + ] + }, + { + "name": "temperature", + "roles": [ + "a", + "b" + ] + }, + { + "name": "url", + "roles": [ + "oldURL" + ] + } + ], + "utterances": [ + { + "text": "\" i need to know the temperature at bangor , sme \"", + "intent": "Weather.GetForecast", + "entities": [] + }, + { + "text": "\" tell me perth weather , sclimate & temperature at australia \"", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 10, + "endPos": 14, + "children": [] + } + ] + }, + { + "text": "$2 and $4.25", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "$4 and $4.25", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "$4 and $99", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "$4.25 and $4", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "$99 and $4.50", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "10 years old and 4 years old", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "12 years old and 3 days old", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "12 years old and 3 days old and monday july 3rd and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "intent": "EntityTests", + "entities": [ + { + "entity": "Composite1", + "startPos": 0, + "endPos": 299, + "children": [] + } + ] + }, + { + "text": "12% and 8%", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "12% to 8%", + "intent": "Roles", + "entities": [ + { + "entity": "percentage", + "role": "minimum", + "startPos": 0, + "endPos": 2, + "children": [] + }, + { + "entity": "percentage", + "role": "maximum", + "startPos": 7, + "endPos": 8, + "children": [] + } + ] + }, + { + "text": "3 degrees and -27.5 degrees c", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "3 inches long by 2 inches wide", + "intent": "Roles", + "entities": [ + { + "entity": "dimension", + "role": "length", + "startPos": 0, + "endPos": 7, + "children": [] + }, + { + "entity": "dimension", + "role": "width", + "startPos": 17, + "endPos": 24, + "children": [] + } + ] + }, + { + "text": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com", + "intent": "Roles", + "entities": [ + { + "entity": "dimension", + "role": "length", + "startPos": 0, + "endPos": 7, + "children": [] + }, + { + "entity": "dimension", + "role": "width", + "startPos": 17, + "endPos": 24, + "children": [] + }, + { + "entity": "percentage", + "role": "minimum", + "startPos": 35, + "endPos": 36, + "children": [] + }, + { + "entity": "percentage", + "role": "maximum", + "startPos": 41, + "endPos": 43, + "children": [] + }, + { + "entity": "age", + "role": "begin", + "startPos": 65, + "endPos": 75, + "children": [] + }, + { + "entity": "age", + "role": "end", + "startPos": 81, + "endPos": 91, + "children": [] + }, + { + "entity": "Part", + "role": "buy", + "startPos": 119, + "endPos": 123, + "children": [] + }, + { + "entity": "Airline", + "role": "Buyer", + "startPos": 173, + "endPos": 177, + "children": [] + }, + { + "entity": "Airline", + "role": "Seller", + "startPos": 183, + "endPos": 188, + "children": [] + }, + { + "entity": "Weather.Location", + "role": "source", + "startPos": 212, + "endPos": 217, + "children": [] + }, + { + "entity": "Weather.Location", + "role": "destination", + "startPos": 226, + "endPos": 232, + "children": [] + }, + { + "entity": "temperature", + "role": "b", + "startPos": 314, + "endPos": 323, + "children": [] + }, + { + "entity": "Name", + "role": "liker", + "startPos": 329, + "endPos": 332, + "children": [] + }, + { + "entity": "Name", + "role": "likee", + "startPos": 340, + "endPos": 343, + "children": [] + }, + { + "entity": "datetimeV2", + "role": "leave", + "startPos": 355, + "endPos": 357, + "children": [] + }, + { + "entity": "datetimeV2", + "role": "arrive", + "startPos": 370, + "endPos": 372, + "children": [] + }, + { + "entity": "money", + "role": "min", + "startPos": 390, + "endPos": 393, + "children": [] + }, + { + "entity": "money", + "role": "max", + "startPos": 399, + "endPos": 402, + "children": [] + }, + { + "entity": "email", + "role": "receiver", + "startPos": 413, + "endPos": 430, + "children": [] + } + ] + }, + { + "text": "4% and 5%", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "4% to 12%", + "intent": "Roles", + "entities": [ + { + "entity": "percentage", + "role": "minimum", + "startPos": 0, + "endPos": 1, + "children": [] + }, + { + "entity": "percentage", + "role": "maximum", + "startPos": 6, + "endPos": 8, + "children": [] + } + ] + }, + { + "text": "425-555-1212", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "425-555-1234", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "5% to 10%", + "intent": "Roles", + "entities": [ + { + "entity": "percentage", + "role": "minimum", + "startPos": 0, + "endPos": 1, + "children": [] + }, + { + "entity": "percentage", + "role": "maximum", + "startPos": 6, + "endPos": 8, + "children": [] + } + ] + }, + { + "text": "8% and 12%", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "8% and 14%", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "8% and 9%", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "8% to 12%", + "intent": "Roles", + "entities": [ + { + "entity": "percentage", + "role": "minimum", + "startPos": 0, + "endPos": 1, + "children": [] + }, + { + "entity": "percentage", + "role": "maximum", + "startPos": 6, + "endPos": 8, + "children": [] + } + ] + }, + { + "text": "9 feet long by 4 feet wide", + "intent": "Roles", + "entities": [ + { + "entity": "dimension", + "role": "length", + "startPos": 0, + "endPos": 5, + "children": [] + }, + { + "entity": "dimension", + "role": "width", + "startPos": 15, + "endPos": 20, + "children": [] + } + ] + }, + { + "text": "9.2% and 10.3%", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "abort", + "intent": "Cancel", + "entities": [] + }, + { + "text": "and $10 and $20", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "and $4 and $4.25", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "and 4$", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "and 425-765-5555", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "and can i trade kb457 for kb922", + "intent": "Roles", + "entities": [ + { + "entity": "Part", + "role": "sell", + "startPos": 16, + "endPos": 20, + "children": [] + }, + { + "entity": "Part", + "role": "buy", + "startPos": 26, + "endPos": 30, + "children": [] + } + ] + }, + { + "text": "are you between 13 years old and 16 years old", + "intent": "Roles", + "entities": [ + { + "entity": "age", + "role": "begin", + "startPos": 16, + "endPos": 27, + "children": [] + }, + { + "entity": "age", + "role": "end", + "startPos": 33, + "endPos": 44, + "children": [] + } + ] + }, + { + "text": "are you between 4 years old and 7 years old", + "intent": "Roles", + "entities": [ + { + "entity": "age", + "role": "begin", + "startPos": 16, + "endPos": 26, + "children": [] + }, + { + "entity": "age", + "role": "end", + "startPos": 32, + "endPos": 42, + "children": [] + } + ] + }, + { + "text": "are you between 6 years old and 10 years old", + "intent": "Roles", + "entities": [ + { + "entity": "age", + "role": "begin", + "startPos": 16, + "endPos": 26, + "children": [] + }, + { + "entity": "age", + "role": "end", + "startPos": 32, + "endPos": 43, + "children": [] + } + ] + }, + { + "text": "are you between 6 years old and 8 years old", + "intent": "Roles", + "entities": [ + { + "entity": "age", + "role": "end", + "startPos": 32, + "endPos": 42, + "children": [] + } + ] + }, + { + "text": "assist", + "intent": "Help", + "entities": [] + }, + { + "text": "bart simpson", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "bart simpson helps homer simpson", + "intent": "Roles", + "entities": [ + { + "entity": "personName", + "role": "child", + "startPos": 0, + "endPos": 11, + "children": [] + }, + { + "entity": "personName", + "role": "parent", + "startPos": 19, + "endPos": 31, + "children": [] + } + ] + }, + { + "text": "bart simpson is parent of lisa simpson to move calcutta to london", + "intent": "Roles", + "entities": [ + { + "entity": "personName", + "role": "parent", + "startPos": 0, + "endPos": 11, + "children": [] + }, + { + "entity": "personName", + "role": "child", + "startPos": 26, + "endPos": 37, + "children": [] + }, + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 47, + "endPos": 54, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 59, + "endPos": 64, + "children": [] + } + ] + }, + { + "text": "calcutta", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "can i trade kb457 for kb922", + "intent": "Roles", + "entities": [ + { + "entity": "Part", + "role": "sell", + "startPos": 12, + "endPos": 16, + "children": [] + }, + { + "entity": "Part", + "role": "buy", + "startPos": 22, + "endPos": 26, + "children": [] + } + ] + }, + { + "text": "can i trade kb922 for kb457", + "intent": "Roles", + "entities": [ + { + "entity": "Part", + "role": "sell", + "startPos": 12, + "endPos": 16, + "children": [] + }, + { + "entity": "Part", + "role": "buy", + "startPos": 22, + "endPos": 26, + "children": [] + } + ] + }, + { + "text": "cancel", + "intent": "Delivery", + "entities": [] + }, + { + "text": "change 425-765-1111 to 425-888-4444", + "intent": "Roles", + "entities": [ + { + "entity": "phonenumber", + "role": "old", + "startPos": 7, + "endPos": 18, + "children": [] + }, + { + "entity": "phonenumber", + "role": "newPhone", + "startPos": 23, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "change 425-777-1212 to 206-666-4123", + "intent": "Roles", + "entities": [ + { + "entity": "phonenumber", + "role": "old", + "startPos": 7, + "endPos": 18, + "children": [] + }, + { + "entity": "phonenumber", + "role": "newPhone", + "startPos": 23, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "deliver 12345 va to 12346 wa", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 8, + "endPos": 15, + "children": [] + }, + { + "entity": "State", + "startPos": 14, + "endPos": 15, + "children": [] + }, + { + "entity": "Address", + "startPos": 20, + "endPos": 27, + "children": [] + }, + { + "entity": "State", + "startPos": 26, + "endPos": 27, + "children": [] + } + ] + }, + { + "text": "delivery address is in 45654 ga", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 23, + "endPos": 30, + "children": [] + }, + { + "entity": "State", + "startPos": 29, + "endPos": 30, + "children": [] + } + ] + }, + { + "text": "did delta buy virgin", + "intent": "Roles", + "entities": [] + }, + { + "text": "did delta buy virgin?", + "intent": "Roles", + "entities": [ + { + "entity": "Airline", + "role": "Buyer", + "startPos": 4, + "endPos": 8, + "children": [] + }, + { + "entity": "Airline", + "role": "Seller", + "startPos": 14, + "endPos": 19, + "children": [] + } + ] + }, + { + "text": "did the rain from hawaii get to redmond", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "role": "source", + "startPos": 18, + "endPos": 23, + "children": [] + }, + { + "entity": "Weather.Location", + "role": "destination", + "startPos": 32, + "endPos": 38, + "children": [] + } + ] + }, + { + "text": "did virgin buy delta", + "intent": "Roles", + "entities": [ + { + "entity": "Airline", + "role": "Buyer", + "startPos": 4, + "endPos": 9, + "children": [] + }, + { + "entity": "Airline", + "role": "Seller", + "startPos": 15, + "endPos": 19, + "children": [] + } + ] + }, + { + "text": "disregard", + "intent": "Cancel", + "entities": [] + }, + { + "text": "do not do it", + "intent": "Cancel", + "entities": [] + }, + { + "text": "do not do that", + "intent": "Cancel", + "entities": [] + }, + { + "text": "don't", + "intent": "Cancel", + "entities": [] + }, + { + "text": "don't do it", + "intent": "Cancel", + "entities": [] + }, + { + "text": "don't do that", + "intent": "Cancel", + "entities": [] + }, + { + "text": "email about dogs from chris and also cats", + "intent": "search", + "entities": [] + }, + { + "text": "fly from paris, texas to chicago via stork", + "intent": "EntityTests", + "entities": [ + { + "entity": "Composite2", + "startPos": 0, + "endPos": 41, + "children": [] + } + ] + }, + { + "text": "forecast in celcius", + "intent": "Weather.GetForecast", + "entities": [] + }, + { + "text": "get florence temperature in september", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 4, + "endPos": 11, + "children": [] + } + ] + }, + { + "text": "get for me the weather conditions in sonoma county", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 37, + "endPos": 49, + "children": [] + } + ] + }, + { + "text": "get the daily temperature greenwood indiana", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 26, + "endPos": 42, + "children": [] + } + ] + }, + { + "text": "get the forcast for me", + "intent": "Weather.GetForecast", + "entities": [] + }, + { + "text": "get the weather at saint george utah", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 19, + "endPos": 35, + "children": [] + } + ] + }, + { + "text": "go from 3rd to 5th", + "intent": "Roles", + "entities": [ + { + "entity": "ordinalV2", + "role": "startpos", + "startPos": 8, + "endPos": 10, + "children": [] + }, + { + "entity": "ordinalV2", + "role": "endpos", + "startPos": 15, + "endPos": 17, + "children": [] + } + ] + }, + { + "text": "go from first to last", + "intent": "Roles", + "entities": [ + { + "entity": "ordinalV2", + "role": "startpos", + "startPos": 8, + "endPos": 12, + "children": [] + }, + { + "entity": "ordinalV2", + "role": "endpos", + "startPos": 17, + "endPos": 20, + "children": [] + } + ] + }, + { + "text": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson", + "intent": "Roles", + "entities": [ + { + "entity": "ordinalV2", + "role": "startpos", + "startPos": 8, + "endPos": 19, + "children": [] + }, + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 34, + "endPos": 39, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 44, + "endPos": 50, + "children": [] + }, + { + "entity": "personName", + "role": "parent", + "startPos": 56, + "endPos": 68, + "children": [] + }, + { + "entity": "personName", + "role": "child", + "startPos": 87, + "endPos": 98, + "children": [] + } + ] + }, + { + "text": "good afternoon", + "intent": "Greeting", + "entities": [] + }, + { + "text": "good evening", + "intent": "Greeting", + "entities": [] + }, + { + "text": "good morning", + "intent": "Greeting", + "entities": [] + }, + { + "text": "good night", + "intent": "Greeting", + "entities": [] + }, + { + "text": "he is yousef", + "intent": "SpecifyName", + "entities": [ + { + "entity": "Name", + "startPos": 6, + "endPos": 11, + "children": [] + } + ] + }, + { + "text": "hello", + "intent": "Cancel", + "entities": [] + }, + { + "text": "hello bot", + "intent": "Greeting", + "entities": [] + }, + { + "text": "help", + "intent": "Help", + "entities": [] + }, + { + "text": "help me", + "intent": "Help", + "entities": [] + }, + { + "text": "help me please", + "intent": "Help", + "entities": [] + }, + { + "text": "help please", + "intent": "Help", + "entities": [] + }, + { + "text": "hi", + "intent": "Greeting", + "entities": [] + }, + { + "text": "hi bot", + "intent": "Greeting", + "entities": [] + }, + { + "text": "hi emad", + "intent": "Greeting", + "entities": [] + }, + { + "text": "his name is tom", + "intent": "SpecifyName", + "entities": [ + { + "entity": "Name", + "startPos": 12, + "endPos": 14, + "children": [] + } + ] + }, + { + "text": "hiya", + "intent": "Greeting", + "entities": [] + }, + { + "text": "homer simpson is parent of bart simpson", + "intent": "Roles", + "entities": [] + }, + { + "text": "homer simpson is parent of bart simpson to move jakarta to calcutta", + "intent": "Roles", + "entities": [ + { + "entity": "personName", + "role": "child", + "startPos": 27, + "endPos": 38, + "children": [] + }, + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 48, + "endPos": 54, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 59, + "endPos": 66, + "children": [] + } + ] + }, + { + "text": "homer simpson is parent of lisa simpson", + "intent": "Roles", + "entities": [ + { + "entity": "personName", + "role": "parent", + "startPos": 0, + "endPos": 12, + "children": [] + }, + { + "entity": "personName", + "role": "child", + "startPos": 27, + "endPos": 38, + "children": [] + } + ] + }, + { + "text": "how are you", + "intent": "Greeting", + "entities": [] + }, + { + "text": "how are you doing today?", + "intent": "Greeting", + "entities": [] + }, + { + "text": "how are you doing?", + "intent": "Greeting", + "entities": [] + }, + { + "text": "how are you today?", + "intent": "Greeting", + "entities": [] + }, + { + "text": "how much rain does chambersburg get a year", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 19, + "endPos": 30, + "children": [] + } + ] + }, + { + "text": "howdy", + "intent": "Greeting", + "entities": [] + }, + { + "text": "how's it goig", + "intent": "Greeting", + "entities": [] + }, + { + "text": "http://blah.com changed to http://foo.com", + "intent": "Roles", + "entities": [ + { + "entity": "url", + "role": "oldURL", + "startPos": 0, + "endPos": 14, + "children": [] + } + ] + }, + { + "text": "http://blah.com is where you can fly from dallas to seattle via denver", + "intent": "EntityTests", + "entities": [ + { + "entity": "Composite2", + "startPos": 0, + "endPos": 69, + "children": [] + }, + { + "entity": "City::From", + "startPos": 42, + "endPos": 47, + "children": [] + }, + { + "entity": "City::To", + "startPos": 52, + "endPos": 58, + "children": [] + }, + { + "entity": "City", + "startPos": 64, + "endPos": 69, + "children": [] + } + ] + }, + { + "text": "http://foo.com", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "http://foo.com changed to http://blah.com", + "intent": "Roles", + "entities": [ + { + "entity": "url", + "role": "oldURL", + "startPos": 0, + "endPos": 13, + "children": [] + } + ] + }, + { + "text": "http://foo.com is ok", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "http://foo.com is where you can fly from seattle to dallas via denver", + "intent": "EntityTests", + "entities": [ + { + "entity": "Composite2", + "startPos": 0, + "endPos": 68, + "children": [] + }, + { + "entity": "City::From", + "startPos": 41, + "endPos": 47, + "children": [] + }, + { + "entity": "City::To", + "startPos": 52, + "endPos": 57, + "children": [] + }, + { + "entity": "City", + "startPos": 63, + "endPos": 68, + "children": [] + } + ] + }, + { + "text": "http://woof.com", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "http://woof.com is where you can fly from seattle to dallas via chicago", + "intent": "EntityTests", + "entities": [ + { + "entity": "Composite2", + "startPos": 0, + "endPos": 70, + "children": [] + }, + { + "entity": "City::From", + "startPos": 42, + "endPos": 48, + "children": [] + }, + { + "entity": "City::To", + "startPos": 53, + "endPos": 58, + "children": [] + }, + { + "entity": "City", + "startPos": 64, + "endPos": 70, + "children": [] + } + ] + }, + { + "text": "http://woof.com is where you can fly from seattle to dallas via chicago on delta", + "intent": "EntityTests", + "entities": [ + { + "entity": "Composite2", + "startPos": 0, + "endPos": 79, + "children": [] + }, + { + "entity": "City::From", + "startPos": 42, + "endPos": 48, + "children": [] + }, + { + "entity": "City::To", + "startPos": 53, + "endPos": 58, + "children": [] + }, + { + "entity": "City", + "startPos": 64, + "endPos": 70, + "children": [] + } + ] + }, + { + "text": "https://foo.com is where you can get weather for seattle", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Composite2", + "startPos": 0, + "endPos": 55, + "children": [] + }, + { + "entity": "Weather.Location", + "startPos": 49, + "endPos": 55, + "children": [] + } + ] + }, + { + "text": "i am lili", + "intent": "SpecifyName", + "entities": [ + { + "entity": "Name", + "startPos": 5, + "endPos": 8, + "children": [] + } + ] + }, + { + "text": "i am stuck", + "intent": "Help", + "entities": [] + }, + { + "text": "i like between 68 degrees and 72 degrees", + "intent": "Roles", + "entities": [ + { + "entity": "temperature", + "role": "a", + "startPos": 15, + "endPos": 24, + "children": [] + } + ] + }, + { + "text": "i like between 72 degrees and 80 degrees", + "intent": "Roles", + "entities": [ + { + "entity": "temperature", + "role": "a", + "startPos": 15, + "endPos": 24, + "children": [] + }, + { + "entity": "temperature", + "role": "b", + "startPos": 30, + "endPos": 39, + "children": [] + } + ] + }, + { + "text": "i want this in 98052 wa", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 15, + "endPos": 22, + "children": [] + }, + { + "entity": "State", + "startPos": 21, + "endPos": 22, + "children": [] + } + ] + }, + { + "text": "i want to arrive at newyork", + "intent": "Travel", + "entities": [ + { + "entity": "City::To", + "startPos": 20, + "endPos": 26, + "children": [] + } + ] + }, + { + "text": "i want to fly out of seattle", + "intent": "Travel", + "entities": [ + { + "entity": "City::From", + "startPos": 21, + "endPos": 27, + "children": [] + } + ] + }, + { + "text": "i want to know the temperature at death valley", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 34, + "endPos": 45, + "children": [] + } + ] + }, + { + "text": "i want to travel", + "intent": "Travel", + "entities": [] + }, + { + "text": "i want to travel from seattle to dallas", + "intent": "Travel", + "entities": [ + { + "entity": "City::From", + "startPos": 22, + "endPos": 28, + "children": [] + }, + { + "entity": "City::To", + "startPos": 33, + "endPos": 38, + "children": [] + } + ] + }, + { + "text": "i would like to cancel", + "intent": "Cancel", + "entities": [] + }, + { + "text": "i'll be leaving from cairo to paris", + "intent": "Travel", + "entities": [ + { + "entity": "City::From", + "startPos": 21, + "endPos": 25, + "children": [] + }, + { + "entity": "City::To", + "startPos": 30, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "i'm stuck", + "intent": "Help", + "entities": [] + }, + { + "text": "joeseph@hotmail.com", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "john likes mary", + "intent": "Roles", + "entities": [ + { + "entity": "Name", + "role": "liker", + "startPos": 0, + "endPos": 3, + "children": [] + }, + { + "entity": "Name", + "role": "likee", + "startPos": 11, + "endPos": 14, + "children": [] + } + ] + }, + { + "text": "kb409 is cool", + "intent": "EntityTests", + "entities": [] + }, + { + "text": "leave 3pm and arrive 5pm", + "intent": "Roles", + "entities": [ + { + "entity": "datetimeV2", + "role": "leave", + "startPos": 6, + "endPos": 8, + "children": [] + }, + { + "entity": "datetimeV2", + "role": "arrive", + "startPos": 21, + "endPos": 23, + "children": [] + } + ] + }, + { + "text": "mayday", + "intent": "Help", + "entities": [] + }, + { + "text": "move calcutta to mumbai", + "intent": "Roles", + "entities": [ + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 5, + "endPos": 12, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 17, + "endPos": 22, + "children": [] + } + ] + }, + { + "text": "move jakarta to london", + "intent": "Roles", + "entities": [ + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 5, + "endPos": 11, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 16, + "endPos": 21, + "children": [] + } + ] + }, + { + "text": "move london to calcutta", + "intent": "Roles", + "entities": [ + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 5, + "endPos": 10, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 15, + "endPos": 22, + "children": [] + } + ] + }, + { + "text": "move london to jakarta", + "intent": "Roles", + "entities": [ + { + "entity": "geographyV2", + "role": "startloc", + "startPos": 5, + "endPos": 10, + "children": [] + }, + { + "entity": "geographyV2", + "role": "endloc", + "startPos": 15, + "endPos": 21, + "children": [] + } + ] + }, + { + "text": "my name is emad", + "intent": "SpecifyName", + "entities": [ + { + "entity": "Name", + "startPos": 11, + "endPos": 14, + "children": [] + } + ] + }, + { + "text": "my water bottle is green.", + "intent": "None", + "entities": [] + }, + { + "text": "never mind", + "intent": "Cancel", + "entities": [] + }, + { + "text": "no thanks", + "intent": "Cancel", + "entities": [] + }, + { + "text": "nope", + "intent": "Cancel", + "entities": [] + }, + { + "text": "pay between $4 and $4.25", + "intent": "Roles", + "entities": [ + { + "entity": "money", + "role": "min", + "startPos": 12, + "endPos": 13, + "children": [] + }, + { + "entity": "money", + "role": "max", + "startPos": 19, + "endPos": 23, + "children": [] + } + ] + }, + { + "text": "pay between $4 and $40", + "intent": "Roles", + "entities": [ + { + "entity": "money", + "role": "min", + "startPos": 12, + "endPos": 13, + "children": [] + }, + { + "entity": "money", + "role": "max", + "startPos": 19, + "endPos": 21, + "children": [] + } + ] + }, + { + "text": "pay between $400 and $500", + "intent": "Roles", + "entities": [ + { + "entity": "money", + "role": "min", + "startPos": 12, + "endPos": 15, + "children": [] + }, + { + "entity": "money", + "role": "max", + "startPos": 21, + "endPos": 24, + "children": [] + } + ] + }, + { + "text": "please cancel", + "intent": "Cancel", + "entities": [] + }, + { + "text": "please deliver february 2nd 2001", + "intent": "Delivery", + "entities": [] + }, + { + "text": "please deliver to 98033 wa", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 18, + "endPos": 25, + "children": [] + }, + { + "entity": "State", + "startPos": 24, + "endPos": 25, + "children": [] + } + ] + }, + { + "text": "please delivery it to 98033 wa", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 22, + "endPos": 29, + "children": [] + }, + { + "entity": "State", + "startPos": 28, + "endPos": 29, + "children": [] + } + ] + }, + { + "text": "please disregard", + "intent": "Cancel", + "entities": [] + }, + { + "text": "please help me", + "intent": "Help", + "entities": [] + }, + { + "text": "please stop", + "intent": "Cancel", + "entities": [] + }, + { + "text": "provide me by toronto weather please", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 14, + "endPos": 20, + "children": [] + } + ] + }, + { + "text": "send bob@foo.com from chris@ark.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 15, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 22, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "send bob@hob.com from main@gmail.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 15, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 22, + "endPos": 35, + "children": [] + } + ] + }, + { + "text": "send cancel@foo.com from help@h.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 18, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 25, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "send chrimc@hotmail.com from emad@gmail.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 22, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 29, + "endPos": 42, + "children": [] + } + ] + }, + { + "text": "send chris@ark.com from bob@foo.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 17, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 24, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "send ham@ham.com from chr@live.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 15, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 22, + "endPos": 33, + "children": [] + } + ] + }, + { + "text": "send help@h.com from cancel@foo.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 14, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 21, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "send tom@foo.com from john@hotmail.com", + "intent": "Roles", + "entities": [ + { + "entity": "email", + "role": "receiver", + "startPos": 5, + "endPos": 15, + "children": [] + }, + { + "entity": "email", + "role": "sender", + "startPos": 22, + "endPos": 37, + "children": [] + } + ] + }, + { + "text": "show average rainfall for boise", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 26, + "endPos": 30, + "children": [] + } + ] + }, + { + "text": "show me the forecast at alabama", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 24, + "endPos": 30, + "children": [] + } + ] + }, + { + "text": "soliciting today ' s weather", + "intent": "Weather.GetForecast", + "entities": [] + }, + { + "text": "sos", + "intent": "Help", + "entities": [] + }, + { + "text": "start with the first one", + "intent": "Roles", + "entities": [] + }, + { + "text": "stop", + "intent": "Cancel", + "entities": [] + }, + { + "text": "temperature of delhi in celsius please", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 15, + "endPos": 19, + "children": [] + } + ] + }, + { + "text": "the address is 66666 fl", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 15, + "endPos": 22, + "children": [] + }, + { + "entity": "State", + "startPos": 21, + "endPos": 22, + "children": [] + } + ] + }, + { + "text": "there is a large deep dish pizza in your future.", + "intent": "None", + "entities": [] + }, + { + "text": "this is chris", + "intent": "SpecifyName", + "entities": [ + { + "entity": "Name", + "startPos": 8, + "endPos": 12, + "children": [] + } + ] + }, + { + "text": "this is requested in 55555 ny", + "intent": "Delivery", + "entities": [ + { + "entity": "Address", + "startPos": 21, + "endPos": 28, + "children": [] + }, + { + "entity": "State", + "startPos": 27, + "endPos": 28, + "children": [] + } + ] + }, + { + "text": "tom likes susan", + "intent": "Roles", + "entities": [ + { + "entity": "Name", + "role": "liker", + "startPos": 0, + "endPos": 2, + "children": [] + }, + { + "entity": "Name", + "role": "likee", + "startPos": 10, + "endPos": 14, + "children": [] + } + ] + }, + { + "text": "was last year about this time as wet as it is now in the south ?", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 57, + "endPos": 61, + "children": [] + } + ] + }, + { + "text": "what ' s the weather going to be like in hawaii ?", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 41, + "endPos": 46, + "children": [] + } + ] + }, + { + "text": "what ' s the weather like in minneapolis", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 29, + "endPos": 39, + "children": [] + } + ] + }, + { + "text": "what can i say", + "intent": "Help", + "entities": [] + }, + { + "text": "what can you do", + "intent": "Help", + "entities": [] + }, + { + "text": "what can you help me with", + "intent": "Help", + "entities": [] + }, + { + "text": "what do i do now?", + "intent": "Help", + "entities": [] + }, + { + "text": "what do i do?", + "intent": "Help", + "entities": [] + }, + { + "text": "what is the rain volume in sonoma county ?", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 27, + "endPos": 39, + "children": [] + } + ] + }, + { + "text": "what is the weather in redmond ?", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 23, + "endPos": 29, + "children": [] + } + ] + }, + { + "text": "what is the weather today at 10 day durham ?", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 36, + "endPos": 41, + "children": [] + } + ] + }, + { + "text": "what to wear in march in california", + "intent": "None", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 25, + "endPos": 34, + "children": [] + } + ] + }, + { + "text": "what will the weather be tomorrow in accord new york ?", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 37, + "endPos": 51, + "children": [] + } + ] + }, + { + "text": "why doesn't this work ?", + "intent": "Help", + "entities": [] + }, + { + "text": "will it be raining in ranchi", + "intent": "Weather.GetForecast", + "entities": [ + { + "entity": "Weather.Location", + "startPos": 22, + "endPos": 27, + "children": [] + } + ] + }, + { + "text": "will it rain this weekend", + "intent": "Weather.GetForecast", + "entities": [] + }, + { + "text": "will it snow today", + "intent": "Weather.GetForecast", + "entities": [] + } + ], + "versionId": "0.2", + "name": "Contoso App V3", + "desc": "Default Intents for Azure Bot Service V2", + "culture": "en-us", + "tokenizerVersion": "1.0.0", + "patternAnyEntities": [ + { + "name": "person", + "roles": [ + "from", + "to" + ], + "explicitList": [] + }, + { + "name": "subject", + "roles": [ + "extra" + ], + "explicitList": [] + } + ], + "regex_entities": [ + { + "name": "Part", + "regexPattern": "kb[0-9]+", + "roles": [ + "sell", + "buy" + ] + } + ], + "phraselists": [], + "regex_features": [], + "patterns": [ + { + "pattern": "deliver from {Address:Source} to {Address:Destination}", + "intent": "Roles" + }, + { + "pattern": "email from {person:from} to {person:to}", + "intent": "search" + }, + { + "pattern": "email about {subject} [from {person}] [and also {subject:extra}]", + "intent": "search" + } + ], + "settings": [ + { + "name": "UseAllTrainingData", + "value": "true" + } + ] +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/DateTimeReference.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/DateTimeReference.json new file mode 100644 index 000000000..d7c01e6bc --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/DateTimeReference.json @@ -0,0 +1,75 @@ +{ + "entities": { + "Airline": [ + [ + "Delta" + ] + ], + "datetime": [ + { + "timex": [ + "2019-05-06" + ], + "type": "date" + } + ] + }, + "intents": { + "EntityTests": { + "score": 0.190871954 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "fly on delta tomorrow", + "v3": { + "options": { + "DateTimeReference": "05/05/2019 12:00:00", + "IncludeAllIntents": false, + "IncludeAPIResults": true, + "IncludeInstanceData": false, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "Airline": [ + [ + "Delta" + ] + ], + "datetimeV2": [ + { + "type": "date", + "values": [ + { + "resolution": [ + { + "value": "2019-05-06" + } + ], + "timex": "2019-05-06" + } + ] + } + ] + }, + "intents": { + "EntityTests": { + "score": 0.190871954 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "fly on delta tomorrow" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/DynamicListsAndList.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/DynamicListsAndList.json new file mode 100644 index 000000000..e973384fa --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/DynamicListsAndList.json @@ -0,0 +1,221 @@ +{ + "entities": { + "$instance": { + "Airline": [ + { + "endIndex": 21, + "modelType": "List Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 4, + "text": "zimbabwe airlines", + "type": "Airline" + }, + { + "endIndex": 33, + "modelType": "List Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 25, + "text": "deltaair", + "type": "Airline" + } + ], + "endloc": [ + { + "endIndex": 12, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 4, + "text": "zimbabwe", + "type": "builtin.geographyV2.countryRegion" + } + ] + }, + "Airline": [ + [ + "ZimAir" + ], + [ + "DeltaAir" + ] + ], + "endloc": [ + { + "location": "zimbabwe", + "type": "countryRegion" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.006457624 + }, + "Delivery": { + "score": 0.00599454 + }, + "EntityTests": { + "score": 0.084535785 + }, + "Greeting": { + "score": 0.005392684 + }, + "Help": { + "score": 0.005619145 + }, + "None": { + "score": 0.03300121 + }, + "Roles": { + "score": 0.0213384479 + }, + "search": { + "score": 0.000823451439 + }, + "SpecifyName": { + "score": 0.0037871073 + }, + "Travel": { + "score": 0.008902215 + }, + "Weather_GetForecast": { + "score": 0.006632081 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "fly zimbabwe airlines or deltaair", + "v3": { + "options": { + "DynamicLists": [ + { + "listEntityName": "Airline", + "requestLists": [ + { + "canonicalForm": "ZimAir", + "synonyms": [ + "zimbabwe airlines" + ] + }, + { + "canonicalForm": "DeltaAir" + } + ] + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Airline": [ + { + "length": 17, + "modelType": "List Entity Extractor", + "modelTypeId": 5, + "recognitionSources": [ + "model" + ], + "startIndex": 4, + "text": "zimbabwe airlines", + "type": "Airline" + }, + { + "length": 8, + "modelType": "List Entity Extractor", + "modelTypeId": 5, + "recognitionSources": [ + "model" + ], + "startIndex": 25, + "text": "deltaair", + "type": "Airline" + } + ], + "endloc": [ + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "endloc", + "startIndex": 4, + "text": "zimbabwe", + "type": "builtin.geographyV2.countryRegion" + } + ] + }, + "Airline": [ + [ + "ZimAir" + ], + [ + "DeltaAir" + ] + ], + "endloc": [ + { + "type": "countryRegion", + "value": "zimbabwe" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.006457624 + }, + "Delivery": { + "score": 0.00599454 + }, + "EntityTests": { + "score": 0.084535785 + }, + "Greeting": { + "score": 0.005392684 + }, + "Help": { + "score": 0.005619145 + }, + "None": { + "score": 0.03300121 + }, + "Roles": { + "score": 0.0213384479 + }, + "search": { + "score": 0.000823451439 + }, + "SpecifyName": { + "score": 0.0037871073 + }, + "Travel": { + "score": 0.008902215 + }, + "Weather.GetForecast": { + "score": 0.006632081 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "fly zimbabwe airlines or deltaair" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndBuiltin.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndBuiltin.json new file mode 100644 index 000000000..6ab4b476a --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndBuiltin.json @@ -0,0 +1,167 @@ +{ + "entities": { + "$instance": { + "number": [ + { + "endIndex": 7, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 4, + "text": "hul", + "type": "builtin.number" + }, + { + "endIndex": 13, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 12, + "text": "2", + "type": "builtin.number" + } + ] + }, + "number": [ + 8, + 2 + ] + }, + "intents": { + "Cancel": { + "score": 0.006839604 + }, + "Delivery": { + "score": 0.005634159 + }, + "EntityTests": { + "score": 0.1439953 + }, + "Greeting": { + "score": 0.004496467 + }, + "Help": { + "score": 0.005810102 + }, + "None": { + "score": 0.0134399384 + }, + "Roles": { + "score": 0.0453764722 + }, + "search": { + "score": 0.00117916975 + }, + "SpecifyName": { + "score": 0.00377203338 + }, + "Travel": { + "score": 0.00252068671 + }, + "Weather_GetForecast": { + "score": 0.009093848 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "buy hul and 2 items", + "v3": { + "options": { + "ExternalEntities": [ + { + "entityLength": 3, + "entityName": "number", + "resolution": 8, + "startIndex": 4 + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "number": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 4, + "text": "hul", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 12, + "text": "2", + "type": "builtin.number" + } + ] + }, + "number": [ + 8, + 2 + ] + }, + "intents": { + "Cancel": { + "score": 0.006839604 + }, + "Delivery": { + "score": 0.005634159 + }, + "EntityTests": { + "score": 0.1439953 + }, + "Greeting": { + "score": 0.004496467 + }, + "Help": { + "score": 0.005810102 + }, + "None": { + "score": 0.0134399384 + }, + "Roles": { + "score": 0.0453764722 + }, + "search": { + "score": 0.00117916975 + }, + "SpecifyName": { + "score": 0.00377203338 + }, + "Travel": { + "score": 0.00252068671 + }, + "Weather.GetForecast": { + "score": 0.009093848 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "buy hul and 2 items" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndComposite.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndComposite.json new file mode 100644 index 000000000..0b8a7b5c8 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndComposite.json @@ -0,0 +1,196 @@ +{ + "entities": { + "$instance": { + "Address": [ + { + "endIndex": 33, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 17, + "text": "repent harelquin", + "type": "Address" + } + ], + "number": [ + { + "endIndex": 10, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "35", + "type": "builtin.number" + } + ] + }, + "Address": [ + { + "number": [ + 3 + ], + "State": [ + "France" + ] + } + ], + "number": [ + 35 + ] + }, + "intents": { + "Cancel": { + "score": 0.00324298278 + }, + "Delivery": { + "score": 0.480763137 + }, + "EntityTests": { + "score": 0.004284346 + }, + "Greeting": { + "score": 0.00282274443 + }, + "Help": { + "score": 0.00290596974 + }, + "None": { + "score": 0.0205373727 + }, + "Roles": { + "score": 0.06780239 + }, + "search": { + "score": 0.000895992853 + }, + "SpecifyName": { + "score": 0.002745299 + }, + "Travel": { + "score": 0.00337635027 + }, + "Weather_GetForecast": { + "score": 0.00949979 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "deliver 35 WA to repent harelquin", + "v3": { + "options": { + "ExternalEntities": [ + { + "entityLength": 16, + "entityName": "Address", + "resolution": { + "number": [ + 3 + ], + "State": [ + "France" + ] + }, + "startIndex": 17 + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Address": [ + { + "length": 16, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 17, + "text": "repent harelquin", + "type": "Address" + } + ], + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "35", + "type": "builtin.number" + } + ] + }, + "Address": [ + { + "number": [ + 3 + ], + "State": [ + "France" + ] + } + ], + "number": [ + 35 + ] + }, + "intents": { + "Cancel": { + "score": 0.00324298278 + }, + "Delivery": { + "score": 0.480763137 + }, + "EntityTests": { + "score": 0.004284346 + }, + "Greeting": { + "score": 0.00282274443 + }, + "Help": { + "score": 0.00290596974 + }, + "None": { + "score": 0.0205373727 + }, + "Roles": { + "score": 0.06780239 + }, + "search": { + "score": 0.000895992853 + }, + "SpecifyName": { + "score": 0.002745299 + }, + "Travel": { + "score": 0.00337635027 + }, + "Weather.GetForecast": { + "score": 0.00949979 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Delivery" + }, + "query": "deliver 35 WA to repent harelquin" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndList.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndList.json new file mode 100644 index 000000000..c4e88f12b --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndList.json @@ -0,0 +1,177 @@ +{ + "entities": { + "$instance": { + "Airline": [ + { + "endIndex": 23, + "modelType": "List Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 7, + "text": "humberg airlines", + "type": "Airline" + }, + { + "endIndex": 32, + "modelType": "List Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 27, + "text": "Delta", + "type": "Airline" + } + ] + }, + "Airline": [ + [ + "HumAir" + ], + [ + "Delta" + ] + ] + }, + "intents": { + "Cancel": { + "score": 0.00322572654 + }, + "Delivery": { + "score": 0.00437863264 + }, + "EntityTests": { + "score": 0.07901226 + }, + "Greeting": { + "score": 0.00273721316 + }, + "Help": { + "score": 0.00294000562 + }, + "None": { + "score": 0.0279089436 + }, + "Roles": { + "score": 0.120735578 + }, + "search": { + "score": 0.000854549464 + }, + "SpecifyName": { + "score": 0.002717544 + }, + "Travel": { + "score": 0.00884681847 + }, + "Weather_GetForecast": { + "score": 0.00485026464 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "fly on humberg airlines or Delta", + "v3": { + "options": { + "ExternalEntities": [ + { + "entityLength": 16, + "entityName": "Airline", + "resolution": [ + "HumAir" + ], + "startIndex": 7 + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Airline": [ + { + "length": 16, + "modelType": "List Entity Extractor", + "modelTypeId": 5, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 7, + "text": "humberg airlines", + "type": "Airline" + }, + { + "length": 5, + "modelType": "List Entity Extractor", + "modelTypeId": 5, + "recognitionSources": [ + "model" + ], + "startIndex": 27, + "text": "Delta", + "type": "Airline" + } + ] + }, + "Airline": [ + [ + "HumAir" + ], + [ + "Delta" + ] + ] + }, + "intents": { + "Cancel": { + "score": 0.00322572654 + }, + "Delivery": { + "score": 0.00437863264 + }, + "EntityTests": { + "score": 0.07901226 + }, + "Greeting": { + "score": 0.00273721316 + }, + "Help": { + "score": 0.00294000562 + }, + "None": { + "score": 0.0279089436 + }, + "Roles": { + "score": 0.120735578 + }, + "search": { + "score": 0.000854549464 + }, + "SpecifyName": { + "score": 0.002717544 + }, + "Travel": { + "score": 0.00884681847 + }, + "Weather.GetForecast": { + "score": 0.00485026464 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Roles" + }, + "query": "fly on humberg airlines or Delta" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndRegex.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndRegex.json new file mode 100644 index 000000000..33b9fef97 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndRegex.json @@ -0,0 +1,166 @@ +{ + "entities": { + "$instance": { + "Part": [ + { + "endIndex": 5, + "modelType": "Regex Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 0, + "text": "42ski", + "type": "Part" + }, + { + "endIndex": 26, + "modelType": "Regex Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 21, + "text": "kb423", + "type": "Part" + } + ] + }, + "Part": [ + "42ski", + "kb423" + ] + }, + "intents": { + "Cancel": { + "score": 0.0128021352 + }, + "Delivery": { + "score": 0.004558195 + }, + "EntityTests": { + "score": 0.009367461 + }, + "Greeting": { + "score": 0.0025622393 + }, + "Help": { + "score": 0.0021368505 + }, + "None": { + "score": 0.2778768 + }, + "Roles": { + "score": 0.0273504611 + }, + "search": { + "score": 0.000832980848 + }, + "SpecifyName": { + "score": 0.00770643726 + }, + "Travel": { + "score": 0.00204822514 + }, + "Weather_GetForecast": { + "score": 0.009585343 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "42ski is a part like kb423", + "v3": { + "options": { + "ExternalEntities": [ + { + "entityLength": 5, + "entityName": "Part", + "startIndex": 0 + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Part": [ + { + "length": 5, + "modelType": "Regex Entity Extractor", + "modelTypeId": 8, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 0, + "text": "42ski", + "type": "Part" + }, + { + "length": 5, + "modelType": "Regex Entity Extractor", + "modelTypeId": 8, + "recognitionSources": [ + "model" + ], + "startIndex": 21, + "text": "kb423", + "type": "Part" + } + ] + }, + "Part": [ + "42ski", + "kb423" + ] + }, + "intents": { + "Cancel": { + "score": 0.0128021352 + }, + "Delivery": { + "score": 0.004558195 + }, + "EntityTests": { + "score": 0.009367461 + }, + "Greeting": { + "score": 0.0025622393 + }, + "Help": { + "score": 0.0021368505 + }, + "None": { + "score": 0.2778768 + }, + "Roles": { + "score": 0.0273504611 + }, + "search": { + "score": 0.000832980848 + }, + "SpecifyName": { + "score": 0.00770643726 + }, + "Travel": { + "score": 0.00204822514 + }, + "Weather.GetForecast": { + "score": 0.009585343 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "None" + }, + "query": "42ski is a part like kb423" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndSimple.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndSimple.json new file mode 100644 index 000000000..7fc6b25c0 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndSimple.json @@ -0,0 +1,232 @@ +{ + "entities": { + "$instance": { + "number": [ + { + "endIndex": 10, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "37", + "type": "builtin.number" + }, + { + "endIndex": 19, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "82", + "type": "builtin.number" + } + ], + "State": [ + { + "endIndex": 13, + "modelType": "Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 11, + "text": "wa", + "type": "State" + }, + { + "endIndex": 22, + "modelType": "Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 20, + "text": "co", + "type": "State" + } + ] + }, + "number": [ + 37, + 82 + ], + "State": [ + "wa", + { + "state": "Colorado" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.004019331 + }, + "Delivery": { + "score": 0.509761333 + }, + "EntityTests": { + "score": 0.004867602 + }, + "Greeting": { + "score": 0.002855288 + }, + "Help": { + "score": 0.00350000733 + }, + "None": { + "score": 0.0121234478 + }, + "Roles": { + "score": 0.08292572 + }, + "search": { + "score": 0.0009203224 + }, + "SpecifyName": { + "score": 0.00308484654 + }, + "Travel": { + "score": 0.00362442387 + }, + "Weather_GetForecast": { + "score": 0.01115346 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "deliver 37 wa to 82 co", + "v3": { + "options": { + "ExternalEntities": [ + { + "entityLength": 2, + "entityName": "State", + "startIndex": 11 + }, + { + "entityLength": 2, + "entityName": "State", + "resolution": { + "state": "Colorado" + }, + "startIndex": 20 + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "37", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "82", + "type": "builtin.number" + } + ], + "State": [ + { + "length": 2, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 11, + "text": "wa", + "type": "State" + }, + { + "length": 2, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 20, + "text": "co", + "type": "State" + } + ] + }, + "number": [ + 37, + 82 + ], + "State": [ + "wa", + { + "state": "Colorado" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.004019331 + }, + "Delivery": { + "score": 0.509761333 + }, + "EntityTests": { + "score": 0.004867602 + }, + "Greeting": { + "score": 0.002855288 + }, + "Help": { + "score": 0.00350000733 + }, + "None": { + "score": 0.0121234478 + }, + "Roles": { + "score": 0.08292572 + }, + "search": { + "score": 0.0009203224 + }, + "SpecifyName": { + "score": 0.00308484654 + }, + "Travel": { + "score": 0.00362442387 + }, + "Weather.GetForecast": { + "score": 0.01115346 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Delivery" + }, + "query": "deliver 37 wa to 82 co" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndSimpleOverride.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndSimpleOverride.json new file mode 100644 index 000000000..5f2080c8d --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalEntitiesAndSimpleOverride.json @@ -0,0 +1,239 @@ +{ + "entities": { + "$instance": { + "number": [ + { + "endIndex": 10, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "37", + "type": "builtin.number" + }, + { + "endIndex": 19, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "82", + "type": "builtin.number" + } + ], + "State": [ + { + "endIndex": 13, + "modelType": "Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 11, + "text": "wa", + "type": "State" + }, + { + "endIndex": 22, + "modelType": "Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 20, + "text": "co", + "type": "State" + } + ] + }, + "number": [ + 37, + 82 + ], + "State": [ + { + "state": "Washington" + }, + { + "state": "Colorado" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.004019331 + }, + "Delivery": { + "score": 0.509761333 + }, + "EntityTests": { + "score": 0.004867602 + }, + "Greeting": { + "score": 0.002855288 + }, + "Help": { + "score": 0.00350000733 + }, + "None": { + "score": 0.0121234478 + }, + "Roles": { + "score": 0.08292572 + }, + "search": { + "score": 0.0009203224 + }, + "SpecifyName": { + "score": 0.00308484654 + }, + "Travel": { + "score": 0.00362442387 + }, + "Weather_GetForecast": { + "score": 0.01115346 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "deliver 37 wa to 82 co", + "v3": { + "options": { + "ExternalEntities": [ + { + "entityLength": 2, + "entityName": "State", + "resolution": { + "state": "Washington" + }, + "startIndex": 11 + }, + { + "entityLength": 2, + "entityName": "State", + "resolution": { + "state": "Colorado" + }, + "startIndex": 20 + } + ], + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "37", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "82", + "type": "builtin.number" + } + ], + "State": [ + { + "length": 2, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 11, + "text": "wa", + "type": "State" + }, + { + "length": 2, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 20, + "text": "co", + "type": "State" + } + ] + }, + "number": [ + 37, + 82 + ], + "State": [ + { + "state": "Washington" + }, + { + "state": "Colorado" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.004019331 + }, + "Delivery": { + "score": 0.509761333 + }, + "EntityTests": { + "score": 0.004867602 + }, + "Greeting": { + "score": 0.002855288 + }, + "Help": { + "score": 0.00350000733 + }, + "None": { + "score": 0.0121234478 + }, + "Roles": { + "score": 0.08292572 + }, + "search": { + "score": 0.0009203224 + }, + "SpecifyName": { + "score": 0.00308484654 + }, + "Travel": { + "score": 0.00362442387 + }, + "Weather.GetForecast": { + "score": 0.01115346 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Delivery" + }, + "query": "deliver 37 wa to 82 co" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalRecognizer.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalRecognizer.json new file mode 100644 index 000000000..05cca17b7 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/ExternalRecognizer.json @@ -0,0 +1,201 @@ +{ + "entities": { + "$instance": { + "Address": [ + { + "endIndex": 33, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 17, + "text": "repent harelquin", + "type": "Address" + } + ], + "number": [ + { + "endIndex": 10, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "35", + "type": "builtin.number" + } + ] + }, + "Address": [ + { + "number": [ + 3 + ], + "State": [ + "France" + ] + } + ], + "number": [ + 35 + ] + }, + "intents": { + "Cancel": { + "score": 0.00324298278 + }, + "Delivery": { + "score": 0.480763137 + }, + "EntityTests": { + "score": 0.004284346 + }, + "Greeting": { + "score": 0.00282274443 + }, + "Help": { + "score": 0.00290596974 + }, + "None": { + "score": 0.0205373727 + }, + "Roles": { + "score": 0.06780239 + }, + "search": { + "score": 0.000895992853 + }, + "SpecifyName": { + "score": 0.002745299 + }, + "Travel": { + "score": 0.00337635027 + }, + "Weather_GetForecast": { + "score": 0.00949979 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "deliver 35 WA to repent harelquin", + "v3": { + "options": { + "ExternalRecognizerResult": { + "$instance":{ + "Address":[ + { + "endIndex":33, + "modelType":"Composite Entity Extractor", + "resolution": { + "number": [ + 3 + ], + "State": [ + "France" + ]}, + "startIndex":17, + "text":"repent harelquin", + "type":"Address" + } + ] + } + }, + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Address": [ + { + "length": 16, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "externalEntities" + ], + "startIndex": 17, + "text": "repent harelquin", + "type": "Address" + } + ], + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "35", + "type": "builtin.number" + } + ] + }, + "Address": [ + { + "number": [ + 3 + ], + "State": [ + "France" + ] + } + ], + "number": [ + 35 + ] + }, + "intents": { + "Cancel": { + "score": 0.00324298278 + }, + "Delivery": { + "score": 0.480763137 + }, + "EntityTests": { + "score": 0.004284346 + }, + "Greeting": { + "score": 0.00282274443 + }, + "Help": { + "score": 0.00290596974 + }, + "None": { + "score": 0.0205373727 + }, + "Roles": { + "score": 0.06780239 + }, + "search": { + "score": 0.000895992853 + }, + "SpecifyName": { + "score": 0.002745299 + }, + "Travel": { + "score": 0.00337635027 + }, + "Weather.GetForecast": { + "score": 0.00949979 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Delivery" + }, + "query": "deliver 35 WA to repent harelquin" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/GeoPeopleOrdinal.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/GeoPeopleOrdinal.json new file mode 100644 index 000000000..758c2efb5 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/GeoPeopleOrdinal.json @@ -0,0 +1,436 @@ +{ + "entities": { + "$instance": { + "child": [ + { + "endIndex": 99, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 87, + "text": "lisa simpson", + "type": "builtin.personName" + } + ], + "endloc": [ + { + "endIndex": 51, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 44, + "text": "jakarta", + "type": "builtin.geographyV2.city" + } + ], + "ordinalV2": [ + { + "endIndex": 28, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 24, + "text": "last", + "type": "builtin.ordinalV2.relative" + } + ], + "parent": [ + { + "endIndex": 69, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 56, + "text": "homer simpson", + "type": "builtin.personName" + } + ], + "startloc": [ + { + "endIndex": 40, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 34, + "text": "london", + "type": "builtin.geographyV2.city" + } + ], + "startpos": [ + { + "endIndex": 20, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 8, + "text": "next to last", + "type": "builtin.ordinalV2.relative" + } + ] + }, + "child": [ + "lisa simpson" + ], + "endloc": [ + { + "location": "jakarta", + "type": "city" + } + ], + "ordinalV2": [ + { + "offset": 0, + "relativeTo": "end" + } + ], + "parent": [ + "homer simpson" + ], + "startloc": [ + { + "location": "london", + "type": "city" + } + ], + "startpos": [ + { + "offset": -1, + "relativeTo": "end" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.000106304564 + }, + "Delivery": { + "score": 0.00121616619 + }, + "EntityTests": { + "score": 0.00107762846 + }, + "Greeting": { + "score": 5.23392373E-05 + }, + "Help": { + "score": 0.000134394242 + }, + "None": { + "score": 0.0108486973 + }, + "Roles": { + "score": 0.9991838 + }, + "search": { + "score": 0.002142746 + }, + "SpecifyName": { + "score": 0.0006965211 + }, + "Travel": { + "score": 0.0046763327 + }, + "Weather_GetForecast": { + "score": 0.0105504664 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "entities": [ + { + "endIndex": 39, + "entity": "london", + "role": "startloc", + "startIndex": 34, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 50, + "entity": "jakarta", + "role": "endloc", + "startIndex": 44, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 19, + "entity": "next to last", + "resolution": { + "offset": "-1", + "relativeTo": "end" + }, + "role": "startpos", + "startIndex": 8, + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 27, + "entity": "last", + "resolution": { + "offset": "0", + "relativeTo": "end" + }, + "startIndex": 24, + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 68, + "entity": "homer simpson", + "role": "parent", + "startIndex": 56, + "type": "builtin.personName" + }, + { + "endIndex": 98, + "entity": "lisa simpson", + "role": "child", + "startIndex": 87, + "type": "builtin.personName" + } + ], + "intents": [ + { + "intent": "Roles", + "score": 0.9991838 + }, + { + "intent": "None", + "score": 0.0108486973 + }, + { + "intent": "Weather.GetForecast", + "score": 0.0105504664 + }, + { + "intent": "Travel", + "score": 0.0046763327 + }, + { + "intent": "search", + "score": 0.002142746 + }, + { + "intent": "Delivery", + "score": 0.00121616619 + }, + { + "intent": "EntityTests", + "score": 0.00107762846 + }, + { + "intent": "SpecifyName", + "score": 0.0006965211 + }, + { + "intent": "Help", + "score": 0.000134394242 + }, + { + "intent": "Cancel", + "score": 0.000106304564 + }, + { + "intent": "Greeting", + "score": 5.23392373E-05 + } + ], + "query": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "Roles", + "score": 0.9991838 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "child": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "child", + "startIndex": 87, + "text": "lisa simpson", + "type": "builtin.personName" + } + ], + "endloc": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "endloc", + "startIndex": 44, + "text": "jakarta", + "type": "builtin.geographyV2.city" + } + ], + "ordinalV2": [ + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 24, + "text": "last", + "type": "builtin.ordinalV2.relative" + } + ], + "parent": [ + { + "length": 13, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "parent", + "startIndex": 56, + "text": "homer simpson", + "type": "builtin.personName" + } + ], + "startloc": [ + { + "length": 6, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "startloc", + "startIndex": 34, + "text": "london", + "type": "builtin.geographyV2.city" + } + ], + "startpos": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "startpos", + "startIndex": 8, + "text": "next to last", + "type": "builtin.ordinalV2.relative" + } + ] + }, + "child": [ + "lisa simpson" + ], + "endloc": [ + { + "type": "city", + "value": "jakarta" + } + ], + "ordinalV2": [ + { + "offset": 0, + "relativeTo": "end" + } + ], + "parent": [ + "homer simpson" + ], + "startloc": [ + { + "type": "city", + "value": "london" + } + ], + "startpos": [ + { + "offset": -1, + "relativeTo": "end" + } + ] + }, + "intents": { + "Cancel": { + "score": 0.000106304564 + }, + "Delivery": { + "score": 0.00121616619 + }, + "EntityTests": { + "score": 0.00107762846 + }, + "Greeting": { + "score": 5.23392373E-05 + }, + "Help": { + "score": 0.000134394242 + }, + "None": { + "score": 0.0108486973 + }, + "Roles": { + "score": 0.9991838 + }, + "search": { + "score": 0.002142746 + }, + "SpecifyName": { + "score": 0.0006965211 + }, + "Travel": { + "score": 0.0046763327 + }, + "Weather.GetForecast": { + "score": 0.0105504664 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Roles" + }, + "query": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Minimal.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Minimal.json new file mode 100644 index 000000000..62a0bb45e --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Minimal.json @@ -0,0 +1,143 @@ +{ + "entities": { + "Airline": [ + [ + "Delta" + ] + ], + "datetime": [ + { + "timex": [ + "T15" + ], + "type": "time" + } + ], + "dimension": [ + { + "number": 3, + "units": "Picometer" + } + ] + }, + "intents": { + "Roles": { + "score": 0.42429316 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "fly on delta at 3pm", + "v2": { + "options": { + "IncludeAllIntents": false, + "IncludeInstanceData": false, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "entities": [ + { + "endIndex": 18, + "entity": "3pm", + "resolution": { + "values": [ + { + "timex": "T15", + "type": "time", + "value": "15:00:00" + } + ] + }, + "startIndex": 16, + "type": "builtin.datetimeV2.time" + }, + { + "endIndex": 18, + "entity": "3pm", + "resolution": { + "unit": "Picometer", + "value": "3" + }, + "startIndex": 16, + "type": "builtin.dimension" + }, + { + "endIndex": 11, + "entity": "delta", + "resolution": { + "values": [ + "Delta" + ] + }, + "startIndex": 7, + "type": "Airline" + } + ], + "query": "fly on delta at 3pm", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "Roles", + "score": 0.42429316 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": false, + "IncludeAPIResults": true, + "IncludeInstanceData": false, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "Airline": [ + [ + "Delta" + ] + ], + "datetimeV2": [ + { + "type": "time", + "values": [ + { + "resolution": [ + { + "value": "15:00:00" + } + ], + "timex": "T15" + } + ] + } + ], + "dimension": [ + { + "number": 3, + "units": "Picometer" + } + ] + }, + "intents": { + "Roles": { + "score": 0.42429316 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Roles" + }, + "query": "fly on delta at 3pm" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/MinimalWithGeo.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/MinimalWithGeo.json new file mode 100644 index 000000000..bd02086e1 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/MinimalWithGeo.json @@ -0,0 +1,672 @@ +{ + "entities": { + "geographyV2": [ + { + "location": "dallas", + "type": "city" + }, + { + "location": "texas", + "type": "state" + } + ] + }, + "intents": { + "EntityTests": { + "score": 0.466911465 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "fly to dallas, texas", + "v2": { + "options": { + "IncludeAllIntents": false, + "IncludeInstanceData": false, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "entities": [ + { + "endIndex": 12, + "entity": "dallas", + "startIndex": 7, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 19, + "entity": "texas", + "startIndex": 15, + "type": "builtin.geographyV2.state" + } + ], + "query": "fly to dallas, texas", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "EntityTests", + "score": 0.466911465 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": false, + "IncludeInstanceData": false, + "LogPersonalInformation": false, + "Slot": "production", + "Timeout": 100000.0 + }, + "response": { + "prediction": { + "entities": { + "Composite1": [ + { + "$instance": { + "age": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old", + "type": "builtin.age" + }, + { + "length": 10, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days old", + "type": "builtin.age" + } + ], + "datetimeV2": [ + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 6, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 21, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 32, + "text": "monday july 3rd, 2019", + "type": "builtin.datetimeV2.date" + }, + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 58, + "text": "every monday", + "type": "builtin.datetimeV2.set" + }, + { + "length": 22, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 75, + "text": "between 3am and 5:30am", + "type": "builtin.datetimeV2.timerange" + } + ], + "dimension": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4 acres", + "type": "builtin.dimension" + }, + { + "length": 13, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4 pico meters", + "type": "builtin.dimension" + } + ], + "email": [ + { + "length": 18, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 132, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "money": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 155, + "text": "$4", + "type": "builtin.currency" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 162, + "text": "$4.25", + "type": "builtin.currency" + } + ], + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "2019", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 91, + "text": "5", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "4", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 163, + "text": "4.25", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 177, + "text": "32", + "type": "builtin.number" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 184, + "text": "210.4", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 222, + "text": "425", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "555", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 230, + "text": "1234", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3", + "type": "builtin.number" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 282, + "text": "one", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 303, + "text": "one", + "type": "builtin.number" + } + ], + "percentage": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10%", + "type": "builtin.percentage" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5%", + "type": "builtin.percentage" + } + ], + "phonenumber": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 222, + "text": "425-555-1234", + "type": "builtin.phonenumber" + } + ], + "temperature": [ + { + "length": 9, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3 degrees", + "type": "builtin.temperature" + }, + { + "length": 15, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5 degrees c", + "type": "builtin.temperature" + } + ] + }, + "age": [ + { + "number": 12, + "unit": "Year" + }, + { + "number": 3, + "unit": "Day" + } + ], + "datetimeV2": [ + { + "type": "duration", + "values": [ + { + "timex": "P12Y", + "value": "378432000" + } + ] + }, + { + "type": "duration", + "values": [ + { + "timex": "P3D", + "value": "259200" + } + ] + }, + { + "type": "date", + "values": [ + { + "timex": "2019-07-03", + "value": "2019-07-03" + } + ] + }, + { + "type": "set", + "values": [ + { + "timex": "XXXX-WXX-1", + "value": "not resolved" + } + ] + }, + { + "type": "timerange", + "values": [ + { + "end": "05:30:00", + "start": "03:00:00", + "timex": "(T03,T05:30,PT2H30M)" + } + ] + } + ], + "dimension": [ + { + "number": 4, + "unit": "Acre" + }, + { + "number": 4, + "unit": "Picometer" + } + ], + "email": [ + "chrimc@hotmail.com" + ], + "money": [ + { + "number": 4, + "unit": "Dollar" + }, + { + "number": 4.25, + "unit": "Dollar" + } + ], + "number": [ + 12, + 3, + 2019, + 5, + 4, + 4, + 4, + 4.25, + 32, + 210.4, + 10, + 10.5, + 425, + 555, + 1234, + 3, + -27.5, + 1, + 1 + ], + "percentage": [ + 10, + 10.5 + ], + "phonenumber": [ + "425-555-1234" + ], + "temperature": [ + { + "number": 3, + "unit": "Degree" + }, + { + "number": -27.5, + "unit": "C" + } + ] + } + ], + "ordinalV2": [ + { + "offset": 3, + "relativeTo": "start" + }, + { + "offset": 1, + "relativeTo": "start" + }, + { + "offset": 1, + "relativeTo": "current" + }, + { + "offset": -1, + "relativeTo": "current" + } + ] + }, + "intents": { + "Cancel": { + "score": 1.56337478E-06 + }, + "Delivery": { + "score": 0.0002846266 + }, + "EntityTests": { + "score": 0.953405857 + }, + "Greeting": { + "score": 8.20979437E-07 + }, + "Help": { + "score": 4.81870757E-06 + }, + "None": { + "score": 0.01040122 + }, + "Roles": { + "score": 0.197366714 + }, + "search": { + "score": 0.14049834 + }, + "SpecifyName": { + "score": 0.000137732946 + }, + "Travel": { + "score": 0.0100996653 + }, + "Weather.GetForecast": { + "score": 0.0143940123 + } + }, + "normalizedQuery": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/NoEntitiesInstanceTrue.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/NoEntitiesInstanceTrue.json new file mode 100644 index 000000000..32313e0e7 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/NoEntitiesInstanceTrue.json @@ -0,0 +1,41 @@ +{ + "entities": { + "$instance": {} + }, + "intents": { + "Greeting": { + "score": 0.959510267 + } + }, + "sentiment": { + "label": "positive", + "score": 0.9431402 + }, + "text": "Hi", + "v3": { + "options": { + "IncludeAllIntents": false, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": {}, + "intents": { + "Greeting": { + "score": 0.959510267 + } + }, + "sentiment": { + "label": "positive", + "score": 0.9431402 + }, + "topIntent": "Greeting" + }, + "query": "Hi" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Patterns.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Patterns.json new file mode 100644 index 000000000..865967c22 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Patterns.json @@ -0,0 +1,363 @@ +{ + "entities": { + "$instance": { + "extra": [ + { + "endIndex": 76, + "modelType": "Pattern.Any Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 71, + "text": "kb435", + "type": "subject" + } + ], + "parent": [ + { + "endIndex": 61, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "bart simpson", + "type": "builtin.personName" + } + ], + "Part": [ + { + "endIndex": 76, + "modelType": "Regex Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 71, + "text": "kb435", + "type": "Part" + } + ], + "person": [ + { + "endIndex": 61, + "modelType": "Pattern.Any Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "bart simpson", + "type": "person" + } + ], + "subject": [ + { + "endIndex": 43, + "modelType": "Pattern.Any Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 12, + "text": "something wicked this way comes", + "type": "subject" + } + ] + }, + "extra": [ + "kb435" + ], + "parent": [ + "bart simpson" + ], + "Part": [ + "kb435" + ], + "person": [ + "bart simpson" + ], + "subject": [ + "something wicked this way comes" + ] + }, + "intents": { + "Cancel": { + "score": 1.02352937E-09 + }, + "Delivery": { + "score": 1.81E-09 + }, + "EntityTests": { + "score": 1.15439843E-05 + }, + "Greeting": { + "score": 1.09375E-09 + }, + "Help": { + "score": 1.02352937E-09 + }, + "None": { + "score": 2.394552E-06 + }, + "Roles": { + "score": 5.6224585E-06 + }, + "search": { + "score": 0.9999948 + }, + "SpecifyName": { + "score": 3.08333337E-09 + }, + "Travel": { + "score": 3.08333337E-09 + }, + "Weather_GetForecast": { + "score": 1.03386708E-06 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "email about something wicked this way comes from bart simpson and also kb435", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "entities": [ + { + "endIndex": 60, + "entity": "bart simpson", + "role": "parent", + "startIndex": 49, + "type": "builtin.personName" + }, + { + "endIndex": 75, + "entity": "kb435", + "startIndex": 71, + "type": "Part" + }, + { + "endIndex": 42, + "entity": "something wicked this way comes", + "role": "", + "startIndex": 12, + "type": "subject" + }, + { + "endIndex": 60, + "entity": "bart simpson", + "role": "", + "startIndex": 49, + "type": "person" + }, + { + "endIndex": 75, + "entity": "kb435", + "role": "extra", + "startIndex": 71, + "type": "subject" + } + ], + "intents": [ + { + "intent": "search", + "score": 0.9999948 + }, + { + "intent": "EntityTests", + "score": 1.15439843E-05 + }, + { + "intent": "Roles", + "score": 5.6224585E-06 + }, + { + "intent": "None", + "score": 2.394552E-06 + }, + { + "intent": "Weather.GetForecast", + "score": 1.03386708E-06 + }, + { + "intent": "SpecifyName", + "score": 3.08333337E-09 + }, + { + "intent": "Travel", + "score": 3.08333337E-09 + }, + { + "intent": "Delivery", + "score": 1.81E-09 + }, + { + "intent": "Greeting", + "score": 1.09375E-09 + }, + { + "intent": "Cancel", + "score": 1.02352937E-09 + }, + { + "intent": "Help", + "score": 1.02352937E-09 + } + ], + "query": "email about something wicked this way comes from bart simpson and also kb435", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "search", + "score": 0.9999948 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "extra": [ + { + "length": 5, + "modelType": "Pattern.Any Entity Extractor", + "modelTypeId": 7, + "recognitionSources": [ + "model" + ], + "role": "extra", + "startIndex": 71, + "text": "kb435", + "type": "subject" + } + ], + "parent": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "parent", + "startIndex": 49, + "text": "bart simpson", + "type": "builtin.personName" + } + ], + "Part": [ + { + "length": 5, + "modelType": "Regex Entity Extractor", + "modelTypeId": 8, + "recognitionSources": [ + "model" + ], + "startIndex": 71, + "text": "kb435", + "type": "Part" + } + ], + "person": [ + { + "length": 12, + "modelType": "Pattern.Any Entity Extractor", + "modelTypeId": 7, + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "bart simpson", + "type": "person" + } + ], + "subject": [ + { + "length": 31, + "modelType": "Pattern.Any Entity Extractor", + "modelTypeId": 7, + "recognitionSources": [ + "model" + ], + "startIndex": 12, + "text": "something wicked this way comes", + "type": "subject" + } + ] + }, + "extra": [ + "kb435" + ], + "parent": [ + "bart simpson" + ], + "Part": [ + "kb435" + ], + "person": [ + "bart simpson" + ], + "subject": [ + "something wicked this way comes" + ] + }, + "intents": { + "Cancel": { + "score": 1.02352937E-09 + }, + "Delivery": { + "score": 1.81E-09 + }, + "EntityTests": { + "score": 1.15439843E-05 + }, + "Greeting": { + "score": 1.09375E-09 + }, + "Help": { + "score": 1.02352937E-09 + }, + "None": { + "score": 2.394552E-06 + }, + "Roles": { + "score": 5.6224585E-06 + }, + "search": { + "score": 0.9999948 + }, + "SpecifyName": { + "score": 3.08333337E-09 + }, + "Travel": { + "score": 3.08333337E-09 + }, + "Weather.GetForecast": { + "score": 1.03386708E-06 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "search" + }, + "query": "email about something wicked this way comes from bart simpson and also kb435" + } + } +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Prebuilt.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Prebuilt.json new file mode 100644 index 000000000..1ccad7581 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Prebuilt.json @@ -0,0 +1,351 @@ +{ + "entities": { + "$instance": { + "Composite2": [ + { + "endIndex": 66, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com is where you can get a weather forecast for seattle", + "type": "Composite2" + } + ], + "geographyV2": [ + { + "endIndex": 66, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "builtin.geographyV2.city" + } + ], + "oldURL": [ + { + "endIndex": 14, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com", + "type": "builtin.url" + } + ] + }, + "Composite2": [ + { + "$instance": { + "Weather_Location": [ + { + "endIndex": 66, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "Weather.Location" + } + ] + }, + "Weather_Location": [ + "seattle" + ] + } + ], + "geographyV2": [ + { + "location": "seattle", + "type": "city" + } + ], + "oldURL": [ + "http://foo.com" + ] + }, + "intents": { + "Cancel": { + "score": 0.00017013021 + }, + "Delivery": { + "score": 0.00114031672 + }, + "EntityTests": { + "score": 0.286522 + }, + "Greeting": { + "score": 0.000150978623 + }, + "Help": { + "score": 0.000547617 + }, + "None": { + "score": 0.01798658 + }, + "Roles": { + "score": 0.0459664278 + }, + "search": { + "score": 0.0009428267 + }, + "SpecifyName": { + "score": 0.0009960134 + }, + "Travel": { + "score": 0.00235179346 + }, + "Weather_GetForecast": { + "score": 0.6732952 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "http://foo.com is where you can get a weather forecast for seattle", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "compositeEntities": [ + { + "children": [ + { + "type": "Weather.Location", + "value": "seattle" + } + ], + "parentType": "Composite2", + "value": "http : / / foo . com is where you can get a weather forecast for seattle" + } + ], + "entities": [ + { + "endIndex": 65, + "entity": "seattle", + "score": 0.8245291, + "startIndex": 59, + "type": "Weather.Location" + }, + { + "endIndex": 65, + "entity": "http : / / foo . com is where you can get a weather forecast for seattle", + "score": 0.6503277, + "startIndex": 0, + "type": "Composite2" + }, + { + "endIndex": 65, + "entity": "seattle", + "startIndex": 59, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 13, + "entity": "http://foo.com", + "resolution": { + "value": "http://foo.com" + }, + "role": "oldURL", + "startIndex": 0, + "type": "builtin.url" + } + ], + "intents": [ + { + "intent": "Weather.GetForecast", + "score": 0.6732952 + }, + { + "intent": "EntityTests", + "score": 0.286522 + }, + { + "intent": "Roles", + "score": 0.0459664278 + }, + { + "intent": "None", + "score": 0.01798658 + }, + { + "intent": "Travel", + "score": 0.00235179346 + }, + { + "intent": "Delivery", + "score": 0.00114031672 + }, + { + "intent": "SpecifyName", + "score": 0.0009960134 + }, + { + "intent": "search", + "score": 0.0009428267 + }, + { + "intent": "Help", + "score": 0.000547617 + }, + { + "intent": "Cancel", + "score": 0.00017013021 + }, + { + "intent": "Greeting", + "score": 0.000150978623 + } + ], + "query": "http://foo.com is where you can get a weather forecast for seattle", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "Weather.GetForecast", + "score": 0.6732952 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Composite2": [ + { + "length": 66, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com is where you can get a weather forecast for seattle", + "type": "Composite2" + } + ], + "geographyV2": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "builtin.geographyV2.city" + } + ], + "oldURL": [ + { + "length": 14, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "oldURL", + "startIndex": 0, + "text": "http://foo.com", + "type": "builtin.url" + } + ] + }, + "Composite2": [ + { + "$instance": { + "Weather.Location": [ + { + "length": 7, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "Weather.Location" + } + ] + }, + "Weather.Location": [ + "seattle" + ] + } + ], + "geographyV2": [ + { + "type": "city", + "value": "seattle" + } + ], + "oldURL": [ + "http://foo.com" + ] + }, + "intents": { + "Cancel": { + "score": 0.00017013021 + }, + "Delivery": { + "score": 0.00114031672 + }, + "EntityTests": { + "score": 0.286522 + }, + "Greeting": { + "score": 0.000150978623 + }, + "Help": { + "score": 0.000547617 + }, + "None": { + "score": 0.01798658 + }, + "Roles": { + "score": 0.0459664278 + }, + "search": { + "score": 0.0009428267 + }, + "SpecifyName": { + "score": 0.0009960134 + }, + "Travel": { + "score": 0.00235179346 + }, + "Weather.GetForecast": { + "score": 0.6732952 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Weather.GetForecast" + }, + "query": "http://foo.com is where you can get a weather forecast for seattle" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TestRecognizerResultConvert.java b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TestRecognizerResultConvert.java new file mode 100644 index 000000000..8e6f6b4bc --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TestRecognizerResultConvert.java @@ -0,0 +1,15 @@ +package com.microsoft.bot.ai.luis.testdata; + +import com.microsoft.bot.builder.RecognizerConvert; +import com.microsoft.bot.builder.RecognizerResult; + +public class TestRecognizerResultConvert implements RecognizerConvert { + + public String recognizerResultText; + + @Override + public void convert(Object result) { + RecognizerResult castedObject = ((RecognizerResult) result); + recognizerResultText = castedObject.getText(); + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TraceActivity.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TraceActivity.json new file mode 100644 index 000000000..da661fa29 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TraceActivity.json @@ -0,0 +1,247 @@ +{ + "entities": { + "$instance": { + "Name": [ + { + "endIndex": 15, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 11, + "text": "Emad", + "type": "Name" + } + ], + "personName": [ + { + "endIndex": 15, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 11, + "text": "Emad", + "type": "builtin.personName" + } + ] + }, + "Name": [ + "Emad" + ], + "personName": [ + "Emad" + ] + }, + "intents": { + "Cancel": { + "score": 0.00555860251 + }, + "Delivery": { + "score": 0.005572036 + }, + "EntityTests": { + "score": 0.006504555 + }, + "Greeting": { + "score": 0.07429792 + }, + "Help": { + "score": 0.004867298 + }, + "None": { + "score": 0.0109896818 + }, + "Roles": { + "score": 0.0382854156 + }, + "search": { + "score": 0.0006921758 + }, + "SpecifyName": { + "score": 0.794012964 + }, + "Travel": { + "score": 0.00203858572 + }, + "Weather_GetForecast": { + "score": 0.006886828 + } + }, + "sentiment": { + "label": "positive", + "score": 0.7930478 + }, + "text": "My name is Emad", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "entities": [ + { + "endIndex": 14, + "entity": "emad", + "score": 0.980508447, + "startIndex": 11, + "type": "Name" + }, + { + "endIndex": 14, + "entity": "emad", + "startIndex": 11, + "type": "builtin.personName" + } + ], + "intents": [ + { + "intent": "SpecifyName", + "score": 0.794012964 + }, + { + "intent": "Greeting", + "score": 0.07429792 + }, + { + "intent": "Roles", + "score": 0.0382854156 + }, + { + "intent": "None", + "score": 0.0109896818 + }, + { + "intent": "Weather.GetForecast", + "score": 0.006886828 + }, + { + "intent": "EntityTests", + "score": 0.006504555 + }, + { + "intent": "Delivery", + "score": 0.005572036 + }, + { + "intent": "Cancel", + "score": 0.00555860251 + }, + { + "intent": "Help", + "score": 0.004867298 + }, + { + "intent": "Travel", + "score": 0.00203858572 + }, + { + "intent": "search", + "score": 0.0006921758 + } + ], + "query": "My name is Emad", + "sentimentAnalysis": { + "label": "positive", + "score": 0.7930478 + }, + "topScoringIntent": { + "intent": "SpecifyName", + "score": 0.794012964 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Name": [ + { + "length": 4, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "startIndex": 11, + "text": "Emad", + "type": "Name" + } + ], + "personName": [ + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 11, + "text": "Emad", + "type": "builtin.personName" + } + ] + }, + "Name": [ + "Emad" + ], + "personName": [ + "Emad" + ] + }, + "intents": { + "Cancel": { + "score": 0.00555860251 + }, + "Delivery": { + "score": 0.005572036 + }, + "EntityTests": { + "score": 0.006504555 + }, + "Greeting": { + "score": 0.07429792 + }, + "Help": { + "score": 0.004867298 + }, + "None": { + "score": 0.0109896818 + }, + "Roles": { + "score": 0.0382854156 + }, + "search": { + "score": 0.0006921758 + }, + "SpecifyName": { + "score": 0.794012964 + }, + "Travel": { + "score": 0.00203858572 + }, + "Weather.GetForecast": { + "score": 0.006886828 + } + }, + "sentiment": { + "label": "positive", + "score": 0.7930478 + }, + "topIntent": "SpecifyName" + }, + "query": "My name is Emad" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Typed.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Typed.json new file mode 100644 index 000000000..88b2a1002 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/Typed.json @@ -0,0 +1,1971 @@ +{ + "entities": { + "$instance": { + "begin": [ + { + "endIndex": 12, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old", + "type": "builtin.age" + } + ], + "Composite1": [ + { + "endIndex": 306, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "type": "Composite1" + } + ], + "end": [ + { + "endIndex": 27, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days old", + "type": "builtin.age" + } + ], + "endpos": [ + { + "endIndex": 47, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 44, + "text": "3rd", + "type": "builtin.ordinalV2" + } + ], + "max": [ + { + "endIndex": 167, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 162, + "text": "$4.25", + "type": "builtin.currency" + } + ], + "ordinalV2": [ + { + "endIndex": 199, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 194, + "text": "first", + "type": "builtin.ordinalV2" + }, + { + "endIndex": 285, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 277, + "text": "next one", + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 306, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 294, + "text": "previous one", + "type": "builtin.ordinalV2.relative" + } + ] + }, + "begin": [ + { + "number": 12, + "units": "Year" + } + ], + "Composite1": [ + { + "$instance": { + "datetime": [ + { + "endIndex": 8, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years", + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 23, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days", + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 53, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 32, + "text": "monday july 3rd, 2019", + "type": "builtin.datetimeV2.date" + }, + { + "endIndex": 70, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 58, + "text": "every monday", + "type": "builtin.datetimeV2.set" + }, + { + "endIndex": 97, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 75, + "text": "between 3am and 5:30am", + "type": "builtin.datetimeV2.timerange" + } + ], + "dimension": [ + { + "endIndex": 109, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4 acres", + "type": "builtin.dimension" + }, + { + "endIndex": 127, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4 pico meters", + "type": "builtin.dimension" + } + ], + "email": [ + { + "endIndex": 150, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 132, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "money": [ + { + "endIndex": 157, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 155, + "text": "$4", + "type": "builtin.currency" + } + ], + "number": [ + { + "endIndex": 2, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12", + "type": "builtin.number" + }, + { + "endIndex": 18, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3", + "type": "builtin.number" + }, + { + "endIndex": 53, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "2019", + "type": "builtin.number" + }, + { + "endIndex": 92, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 91, + "text": "5", + "type": "builtin.number" + }, + { + "endIndex": 103, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4", + "type": "builtin.number" + }, + { + "endIndex": 115, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4", + "type": "builtin.number" + }, + { + "endIndex": 157, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "4", + "type": "builtin.number" + }, + { + "endIndex": 167, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 163, + "text": "4.25", + "type": "builtin.number" + }, + { + "endIndex": 179, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 177, + "text": "32", + "type": "builtin.number" + }, + { + "endIndex": 189, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 184, + "text": "210.4", + "type": "builtin.number" + }, + { + "endIndex": 206, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10", + "type": "builtin.number" + }, + { + "endIndex": 216, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5", + "type": "builtin.number" + }, + { + "endIndex": 225, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 222, + "text": "425", + "type": "builtin.number" + }, + { + "endIndex": 229, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "555", + "type": "builtin.number" + }, + { + "endIndex": 234, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 230, + "text": "1234", + "type": "builtin.number" + }, + { + "endIndex": 240, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3", + "type": "builtin.number" + }, + { + "endIndex": 258, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5", + "type": "builtin.number" + }, + { + "endIndex": 285, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 282, + "text": "one", + "type": "builtin.number" + }, + { + "endIndex": 306, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 303, + "text": "one", + "type": "builtin.number" + } + ], + "percentage": [ + { + "endIndex": 207, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10%", + "type": "builtin.percentage" + }, + { + "endIndex": 217, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5%", + "type": "builtin.percentage" + } + ], + "phonenumber": [ + { + "endIndex": 234, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 222, + "text": "425-555-1234", + "type": "builtin.phonenumber" + } + ], + "temperature": [ + { + "endIndex": 248, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3 degrees", + "type": "builtin.temperature" + }, + { + "endIndex": 268, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5 degrees c", + "type": "builtin.temperature" + } + ] + }, + "datetime": [ + { + "timex": [ + "P12Y" + ], + "type": "duration" + }, + { + "timex": [ + "P3D" + ], + "type": "duration" + }, + { + "timex": [ + "2019-07-03" + ], + "type": "date" + }, + { + "timex": [ + "XXXX-WXX-1" + ], + "type": "set" + }, + { + "timex": [ + "(T03,T05:30,PT2H30M)" + ], + "type": "timerange" + } + ], + "dimension": [ + { + "number": 4, + "units": "Acre" + }, + { + "number": 4, + "units": "Picometer" + } + ], + "email": [ + "chrimc@hotmail.com" + ], + "money": [ + { + "number": 4, + "units": "Dollar" + } + ], + "number": [ + 12, + 3, + 2019, + 5, + 4, + 4, + 4, + 4.25, + 32, + 210.4, + 10, + 10.5, + 425, + 555, + 1234, + 3, + -27.5, + 1, + 1 + ], + "percentage": [ + 10, + 10.5 + ], + "phonenumber": [ + "425-555-1234" + ], + "temperature": [ + { + "number": 3, + "units": "Degree" + }, + { + "number": -27.5, + "units": "C" + } + ] + } + ], + "end": [ + { + "number": 3, + "units": "Day" + } + ], + "endpos": [ + { + "offset": 3, + "relativeTo": "start" + } + ], + "max": [ + { + "number": 4.25, + "units": "Dollar" + } + ], + "ordinalV2": [ + { + "offset": 1, + "relativeTo": "start" + }, + { + "offset": 1, + "relativeTo": "current" + }, + { + "offset": -1, + "relativeTo": "current" + } + ] + }, + "intents": { + "Cancel": { + "score": 1.54311692E-06 + }, + "Delivery": { + "score": 0.000280677923 + }, + "EntityTests": { + "score": 0.958614767 + }, + "Greeting": { + "score": 8.076372E-07 + }, + "Help": { + "score": 4.74059061E-06 + }, + "None": { + "score": 0.0101076821 + }, + "Roles": { + "score": 0.191202149 + }, + "search": { + "score": 0.00475360872 + }, + "SpecifyName": { + "score": 7.367716E-05 + }, + "Travel": { + "score": 0.00232480234 + }, + "Weather_GetForecast": { + "score": 0.0141556319 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "compositeEntities": [ + { + "children": [ + { + "type": "builtin.datetimeV2.duration", + "value": "12 years" + }, + { + "type": "builtin.datetimeV2.duration", + "value": "3 days" + }, + { + "type": "builtin.datetimeV2.date", + "value": "monday july 3rd, 2019" + }, + { + "type": "builtin.datetimeV2.set", + "value": "every monday" + }, + { + "type": "builtin.datetimeV2.timerange", + "value": "between 3am and 5:30am" + }, + { + "type": "builtin.dimension", + "value": "4 acres" + }, + { + "type": "builtin.dimension", + "value": "4 pico meters" + }, + { + "type": "builtin.email", + "value": "chrimc@hotmail.com" + }, + { + "type": "builtin.currency", + "value": "$4" + }, + { + "type": "builtin.number", + "value": "12" + }, + { + "type": "builtin.number", + "value": "3" + }, + { + "type": "builtin.number", + "value": "2019" + }, + { + "type": "builtin.number", + "value": "5" + }, + { + "type": "builtin.number", + "value": "4" + }, + { + "type": "builtin.number", + "value": "4" + }, + { + "type": "builtin.number", + "value": "4" + }, + { + "type": "builtin.number", + "value": "4.25" + }, + { + "type": "builtin.number", + "value": "32" + }, + { + "type": "builtin.number", + "value": "210.4" + }, + { + "type": "builtin.number", + "value": "10" + }, + { + "type": "builtin.number", + "value": "10.5" + }, + { + "type": "builtin.number", + "value": "425" + }, + { + "type": "builtin.number", + "value": "555" + }, + { + "type": "builtin.number", + "value": "1234" + }, + { + "type": "builtin.number", + "value": "3" + }, + { + "type": "builtin.number", + "value": "-27.5" + }, + { + "type": "builtin.number", + "value": "one" + }, + { + "type": "builtin.number", + "value": "one" + }, + { + "type": "builtin.percentage", + "value": "10%" + }, + { + "type": "builtin.percentage", + "value": "10.5%" + }, + { + "type": "builtin.phonenumber", + "value": "425-555-1234" + }, + { + "type": "builtin.temperature", + "value": "3 degrees" + }, + { + "type": "builtin.temperature", + "value": "-27.5 degrees c" + } + ], + "parentType": "Composite1", + "value": "12 years old and 3 days old and monday july 3rd , 2019 and every monday and between 3am and 5 : 30am and 4 acres and 4 pico meters and chrimc @ hotmail . com and $ 4 and $ 4 . 25 and also 32 and 210 . 4 and first and 10 % and 10 . 5 % and 425 - 555 - 1234 and 3 degrees and - 27 . 5 degrees c and the next one and the previous one" + } + ], + "entities": [ + { + "endIndex": 305, + "entity": "12 years old and 3 days old and monday july 3rd , 2019 and every monday and between 3am and 5 : 30am and 4 acres and 4 pico meters and chrimc @ hotmail . com and $ 4 and $ 4 . 25 and also 32 and 210 . 4 and first and 10 % and 10 . 5 % and 425 - 555 - 1234 and 3 degrees and - 27 . 5 degrees c and the next one and the previous one", + "score": 0.9074669, + "startIndex": 0, + "type": "Composite1" + }, + { + "endIndex": 1, + "entity": "12", + "resolution": { + "subtype": "integer", + "value": "12" + }, + "startIndex": 0, + "type": "builtin.number" + }, + { + "endIndex": 17, + "entity": "3", + "resolution": { + "subtype": "integer", + "value": "3" + }, + "startIndex": 17, + "type": "builtin.number" + }, + { + "endIndex": 52, + "entity": "2019", + "resolution": { + "subtype": "integer", + "value": "2019" + }, + "startIndex": 49, + "type": "builtin.number" + }, + { + "endIndex": 91, + "entity": "5", + "resolution": { + "subtype": "integer", + "value": "5" + }, + "startIndex": 91, + "type": "builtin.number" + }, + { + "endIndex": 102, + "entity": "4", + "resolution": { + "subtype": "integer", + "value": "4" + }, + "startIndex": 102, + "type": "builtin.number" + }, + { + "endIndex": 114, + "entity": "4", + "resolution": { + "subtype": "integer", + "value": "4" + }, + "startIndex": 114, + "type": "builtin.number" + }, + { + "endIndex": 156, + "entity": "4", + "resolution": { + "subtype": "integer", + "value": "4" + }, + "startIndex": 156, + "type": "builtin.number" + }, + { + "endIndex": 166, + "entity": "4.25", + "resolution": { + "subtype": "decimal", + "value": "4.25" + }, + "startIndex": 163, + "type": "builtin.number" + }, + { + "endIndex": 178, + "entity": "32", + "resolution": { + "subtype": "integer", + "value": "32" + }, + "startIndex": 177, + "type": "builtin.number" + }, + { + "endIndex": 188, + "entity": "210.4", + "resolution": { + "subtype": "decimal", + "value": "210.4" + }, + "startIndex": 184, + "type": "builtin.number" + }, + { + "endIndex": 205, + "entity": "10", + "resolution": { + "subtype": "integer", + "value": "10" + }, + "startIndex": 204, + "type": "builtin.number" + }, + { + "endIndex": 215, + "entity": "10.5", + "resolution": { + "subtype": "decimal", + "value": "10.5" + }, + "startIndex": 212, + "type": "builtin.number" + }, + { + "endIndex": 224, + "entity": "425", + "resolution": { + "subtype": "integer", + "value": "425" + }, + "startIndex": 222, + "type": "builtin.number" + }, + { + "endIndex": 228, + "entity": "555", + "resolution": { + "subtype": "integer", + "value": "555" + }, + "startIndex": 226, + "type": "builtin.number" + }, + { + "endIndex": 233, + "entity": "1234", + "resolution": { + "subtype": "integer", + "value": "1234" + }, + "startIndex": 230, + "type": "builtin.number" + }, + { + "endIndex": 239, + "entity": "3", + "resolution": { + "subtype": "integer", + "value": "3" + }, + "startIndex": 239, + "type": "builtin.number" + }, + { + "endIndex": 257, + "entity": "-27.5", + "resolution": { + "subtype": "decimal", + "value": "-27.5" + }, + "startIndex": 253, + "type": "builtin.number" + }, + { + "endIndex": 284, + "entity": "one", + "resolution": { + "subtype": "integer", + "value": "1" + }, + "startIndex": 282, + "type": "builtin.number" + }, + { + "endIndex": 305, + "entity": "one", + "resolution": { + "subtype": "integer", + "value": "1" + }, + "startIndex": 303, + "type": "builtin.number" + }, + { + "endIndex": 11, + "entity": "12 years old", + "resolution": { + "unit": "Year", + "value": "12" + }, + "role": "begin", + "startIndex": 0, + "type": "builtin.age" + }, + { + "endIndex": 26, + "entity": "3 days old", + "resolution": { + "unit": "Day", + "value": "3" + }, + "role": "end", + "startIndex": 17, + "type": "builtin.age" + }, + { + "endIndex": 7, + "entity": "12 years", + "resolution": { + "values": [ + { + "timex": "P12Y", + "type": "duration", + "value": "378432000" + } + ] + }, + "startIndex": 0, + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 22, + "entity": "3 days", + "resolution": { + "values": [ + { + "timex": "P3D", + "type": "duration", + "value": "259200" + } + ] + }, + "startIndex": 17, + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 52, + "entity": "monday july 3rd, 2019", + "resolution": { + "values": [ + { + "timex": "2019-07-03", + "type": "date", + "value": "2019-07-03" + } + ] + }, + "startIndex": 32, + "type": "builtin.datetimeV2.date" + }, + { + "endIndex": 69, + "entity": "every monday", + "resolution": { + "values": [ + { + "timex": "XXXX-WXX-1", + "type": "set", + "value": "not resolved" + } + ] + }, + "startIndex": 58, + "type": "builtin.datetimeV2.set" + }, + { + "endIndex": 96, + "entity": "between 3am and 5:30am", + "resolution": { + "values": [ + { + "end": "05:30:00", + "start": "03:00:00", + "timex": "(T03,T05:30,PT2H30M)", + "type": "timerange" + } + ] + }, + "startIndex": 75, + "type": "builtin.datetimeV2.timerange" + }, + { + "endIndex": 108, + "entity": "4 acres", + "resolution": { + "unit": "Acre", + "value": "4" + }, + "startIndex": 102, + "type": "builtin.dimension" + }, + { + "endIndex": 126, + "entity": "4 pico meters", + "resolution": { + "unit": "Picometer", + "value": "4" + }, + "startIndex": 114, + "type": "builtin.dimension" + }, + { + "endIndex": 149, + "entity": "chrimc@hotmail.com", + "resolution": { + "value": "chrimc@hotmail.com" + }, + "startIndex": 132, + "type": "builtin.email" + }, + { + "endIndex": 156, + "entity": "$4", + "resolution": { + "unit": "Dollar", + "value": "4" + }, + "startIndex": 155, + "type": "builtin.currency" + }, + { + "endIndex": 166, + "entity": "$4.25", + "resolution": { + "unit": "Dollar", + "value": "4.25" + }, + "role": "max", + "startIndex": 162, + "type": "builtin.currency" + }, + { + "endIndex": 46, + "entity": "3rd", + "resolution": { + "offset": "3", + "relativeTo": "start" + }, + "role": "endpos", + "startIndex": 44, + "type": "builtin.ordinalV2" + }, + { + "endIndex": 198, + "entity": "first", + "resolution": { + "offset": "1", + "relativeTo": "start" + }, + "startIndex": 194, + "type": "builtin.ordinalV2" + }, + { + "endIndex": 284, + "entity": "next one", + "resolution": { + "offset": "1", + "relativeTo": "current" + }, + "startIndex": 277, + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 305, + "entity": "previous one", + "resolution": { + "offset": "-1", + "relativeTo": "current" + }, + "startIndex": 294, + "type": "builtin.ordinalV2.relative" + }, + { + "endIndex": 206, + "entity": "10%", + "resolution": { + "value": "10%" + }, + "startIndex": 204, + "type": "builtin.percentage" + }, + { + "endIndex": 216, + "entity": "10.5%", + "resolution": { + "value": "10.5%" + }, + "startIndex": 212, + "type": "builtin.percentage" + }, + { + "endIndex": 233, + "entity": "425-555-1234", + "resolution": { + "score": "0.9", + "value": "425-555-1234" + }, + "startIndex": 222, + "type": "builtin.phonenumber" + }, + { + "endIndex": 247, + "entity": "3 degrees", + "resolution": { + "unit": "Degree", + "value": "3" + }, + "startIndex": 239, + "type": "builtin.temperature" + }, + { + "endIndex": 267, + "entity": "-27.5 degrees c", + "resolution": { + "unit": "C", + "value": "-27.5" + }, + "startIndex": 253, + "type": "builtin.temperature" + } + ], + "intents": [ + { + "intent": "EntityTests", + "score": 0.958614767 + }, + { + "intent": "Roles", + "score": 0.191202149 + }, + { + "intent": "Weather.GetForecast", + "score": 0.0141556319 + }, + { + "intent": "None", + "score": 0.0101076821 + }, + { + "intent": "search", + "score": 0.00475360872 + }, + { + "intent": "Travel", + "score": 0.00232480234 + }, + { + "intent": "Delivery", + "score": 0.000280677923 + }, + { + "intent": "SpecifyName", + "score": 7.367716E-05 + }, + { + "intent": "Help", + "score": 4.74059061E-06 + }, + { + "intent": "Cancel", + "score": 1.54311692E-06 + }, + { + "intent": "Greeting", + "score": 8.076372E-07 + } + ], + "query": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "EntityTests", + "score": 0.958614767 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "begin": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "begin", + "startIndex": 0, + "text": "12 years old", + "type": "builtin.age" + } + ], + "Composite1": [ + { + "length": 306, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one", + "type": "Composite1" + } + ], + "end": [ + { + "length": 10, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "end", + "startIndex": 17, + "text": "3 days old", + "type": "builtin.age" + } + ], + "endpos": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "endpos", + "startIndex": 44, + "text": "3rd", + "type": "builtin.ordinalV2" + } + ], + "max": [ + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "max", + "startIndex": 162, + "text": "$4.25", + "type": "builtin.currency" + } + ], + "ordinalV2": [ + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 194, + "text": "first", + "type": "builtin.ordinalV2" + }, + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 277, + "text": "next one", + "type": "builtin.ordinalV2.relative" + }, + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 294, + "text": "previous one", + "type": "builtin.ordinalV2.relative" + } + ] + }, + "begin": [ + { + "number": 12, + "units": "Year" + } + ], + "Composite1": [ + { + "$instance": { + "datetimeV2": [ + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12 years", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 6, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3 days", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 21, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 32, + "text": "monday july 3rd, 2019", + "type": "builtin.datetimeV2.date" + }, + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 58, + "text": "every monday", + "type": "builtin.datetimeV2.set" + }, + { + "length": 22, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 75, + "text": "between 3am and 5:30am", + "type": "builtin.datetimeV2.timerange" + } + ], + "dimension": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4 acres", + "type": "builtin.dimension" + }, + { + "length": 13, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4 pico meters", + "type": "builtin.dimension" + } + ], + "email": [ + { + "length": 18, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 132, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "money": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 155, + "text": "$4", + "type": "builtin.currency" + } + ], + "number": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "12", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "3", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 49, + "text": "2019", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 91, + "text": "5", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 102, + "text": "4", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 114, + "text": "4", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "4", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 163, + "text": "4.25", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 177, + "text": "32", + "type": "builtin.number" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 184, + "text": "210.4", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 222, + "text": "425", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "555", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 230, + "text": "1234", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3", + "type": "builtin.number" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 282, + "text": "one", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 303, + "text": "one", + "type": "builtin.number" + } + ], + "percentage": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 204, + "text": "10%", + "type": "builtin.percentage" + }, + { + "length": 5, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "10.5%", + "type": "builtin.percentage" + } + ], + "phonenumber": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 222, + "text": "425-555-1234", + "type": "builtin.phonenumber" + } + ], + "temperature": [ + { + "length": 9, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 239, + "text": "3 degrees", + "type": "builtin.temperature" + }, + { + "length": 15, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 253, + "text": "-27.5 degrees c", + "type": "builtin.temperature" + } + ] + }, + "datetimeV2": [ + { + "type": "duration", + "values": [ + { + "resolution": [ + { + "value": "378432000" + } + ], + "timex": "P12Y" + } + ] + }, + { + "type": "duration", + "values": [ + { + "resolution": [ + { + "value": "259200" + } + ], + "timex": "P3D" + } + ] + }, + { + "type": "date", + "values": [ + { + "resolution": [ + { + "value": "2019-07-03" + } + ], + "timex": "2019-07-03" + } + ] + }, + { + "type": "set", + "values": [ + { + "resolution": [ + { + "value": "not resolved" + } + ], + "timex": "XXXX-WXX-1" + } + ] + }, + { + "type": "timerange", + "values": [ + { + "resolution": [ + { + "end": "05:30:00", + "start": "03:00:00" + } + ], + "timex": "(T03,T05:30,PT2H30M)" + } + ] + } + ], + "dimension": [ + { + "number": 4, + "units": "Acre" + }, + { + "number": 4, + "units": "Picometer" + } + ], + "email": [ + "chrimc@hotmail.com" + ], + "money": [ + { + "number": 4, + "units": "Dollar" + } + ], + "number": [ + 12, + 3, + 2019, + 5, + 4, + 4, + 4, + 4.25, + 32, + 210.4, + 10, + 10.5, + 425, + 555, + 1234, + 3, + -27.5, + 1, + 1 + ], + "percentage": [ + 10, + 10.5 + ], + "phonenumber": [ + "425-555-1234" + ], + "temperature": [ + { + "number": 3, + "units": "Degree" + }, + { + "number": -27.5, + "units": "C" + } + ] + } + ], + "end": [ + { + "number": 3, + "units": "Day" + } + ], + "endpos": [ + { + "offset": 3, + "relativeTo": "start" + } + ], + "max": [ + { + "number": 4.25, + "units": "Dollar" + } + ], + "ordinalV2": [ + { + "offset": 1, + "relativeTo": "start" + }, + { + "offset": 1, + "relativeTo": "current" + }, + { + "offset": -1, + "relativeTo": "current" + } + ] + }, + "intents": { + "Cancel": { + "score": 1.54311692E-06 + }, + "Delivery": { + "score": 0.000280677923 + }, + "EntityTests": { + "score": 0.958614767 + }, + "Greeting": { + "score": 8.076372E-07 + }, + "Help": { + "score": 4.74059061E-06 + }, + "None": { + "score": 0.0101076821 + }, + "Roles": { + "score": 0.191202149 + }, + "search": { + "score": 0.00475360872 + }, + "SpecifyName": { + "score": 7.367716E-05 + }, + "Travel": { + "score": 0.00232480234 + }, + "Weather.GetForecast": { + "score": 0.0141556319 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "EntityTests" + }, + "query": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TypedPrebuilt.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TypedPrebuilt.json new file mode 100644 index 000000000..1ccad7581 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/TypedPrebuilt.json @@ -0,0 +1,351 @@ +{ + "entities": { + "$instance": { + "Composite2": [ + { + "endIndex": 66, + "modelType": "Composite Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com is where you can get a weather forecast for seattle", + "type": "Composite2" + } + ], + "geographyV2": [ + { + "endIndex": 66, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "builtin.geographyV2.city" + } + ], + "oldURL": [ + { + "endIndex": 14, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com", + "type": "builtin.url" + } + ] + }, + "Composite2": [ + { + "$instance": { + "Weather_Location": [ + { + "endIndex": 66, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "Weather.Location" + } + ] + }, + "Weather_Location": [ + "seattle" + ] + } + ], + "geographyV2": [ + { + "location": "seattle", + "type": "city" + } + ], + "oldURL": [ + "http://foo.com" + ] + }, + "intents": { + "Cancel": { + "score": 0.00017013021 + }, + "Delivery": { + "score": 0.00114031672 + }, + "EntityTests": { + "score": 0.286522 + }, + "Greeting": { + "score": 0.000150978623 + }, + "Help": { + "score": 0.000547617 + }, + "None": { + "score": 0.01798658 + }, + "Roles": { + "score": 0.0459664278 + }, + "search": { + "score": 0.0009428267 + }, + "SpecifyName": { + "score": 0.0009960134 + }, + "Travel": { + "score": 0.00235179346 + }, + "Weather_GetForecast": { + "score": 0.6732952 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "http://foo.com is where you can get a weather forecast for seattle", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "compositeEntities": [ + { + "children": [ + { + "type": "Weather.Location", + "value": "seattle" + } + ], + "parentType": "Composite2", + "value": "http : / / foo . com is where you can get a weather forecast for seattle" + } + ], + "entities": [ + { + "endIndex": 65, + "entity": "seattle", + "score": 0.8245291, + "startIndex": 59, + "type": "Weather.Location" + }, + { + "endIndex": 65, + "entity": "http : / / foo . com is where you can get a weather forecast for seattle", + "score": 0.6503277, + "startIndex": 0, + "type": "Composite2" + }, + { + "endIndex": 65, + "entity": "seattle", + "startIndex": 59, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 13, + "entity": "http://foo.com", + "resolution": { + "value": "http://foo.com" + }, + "role": "oldURL", + "startIndex": 0, + "type": "builtin.url" + } + ], + "intents": [ + { + "intent": "Weather.GetForecast", + "score": 0.6732952 + }, + { + "intent": "EntityTests", + "score": 0.286522 + }, + { + "intent": "Roles", + "score": 0.0459664278 + }, + { + "intent": "None", + "score": 0.01798658 + }, + { + "intent": "Travel", + "score": 0.00235179346 + }, + { + "intent": "Delivery", + "score": 0.00114031672 + }, + { + "intent": "SpecifyName", + "score": 0.0009960134 + }, + { + "intent": "search", + "score": 0.0009428267 + }, + { + "intent": "Help", + "score": 0.000547617 + }, + { + "intent": "Cancel", + "score": 0.00017013021 + }, + { + "intent": "Greeting", + "score": 0.000150978623 + } + ], + "query": "http://foo.com is where you can get a weather forecast for seattle", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "Weather.GetForecast", + "score": 0.6732952 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "Composite2": [ + { + "length": 66, + "modelType": "Composite Entity Extractor", + "modelTypeId": 4, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "http://foo.com is where you can get a weather forecast for seattle", + "type": "Composite2" + } + ], + "geographyV2": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "builtin.geographyV2.city" + } + ], + "oldURL": [ + { + "length": 14, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "oldURL", + "startIndex": 0, + "text": "http://foo.com", + "type": "builtin.url" + } + ] + }, + "Composite2": [ + { + "$instance": { + "Weather.Location": [ + { + "length": 7, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "startIndex": 59, + "text": "seattle", + "type": "Weather.Location" + } + ] + }, + "Weather.Location": [ + "seattle" + ] + } + ], + "geographyV2": [ + { + "type": "city", + "value": "seattle" + } + ], + "oldURL": [ + "http://foo.com" + ] + }, + "intents": { + "Cancel": { + "score": 0.00017013021 + }, + "Delivery": { + "score": 0.00114031672 + }, + "EntityTests": { + "score": 0.286522 + }, + "Greeting": { + "score": 0.000150978623 + }, + "Help": { + "score": 0.000547617 + }, + "None": { + "score": 0.01798658 + }, + "Roles": { + "score": 0.0459664278 + }, + "search": { + "score": 0.0009428267 + }, + "SpecifyName": { + "score": 0.0009960134 + }, + "Travel": { + "score": 0.00235179346 + }, + "Weather.GetForecast": { + "score": 0.6732952 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Weather.GetForecast" + }, + "query": "http://foo.com is where you can get a weather forecast for seattle" + } + } +} diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/V1DatetimeResolution.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/V1DatetimeResolution.json new file mode 100644 index 000000000..eb662c3ff --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/V1DatetimeResolution.json @@ -0,0 +1,19 @@ +{ + "query": "4", + "topScoringIntent": { + "intent": "None", + "score": 0.8575135 + }, + "entities": [ + { + "entity": "4", + "type": "builtin.datetime.time", + "startIndex": 0, + "endIndex": 0, + "resolution": { + "comment": "ampm", + "time": "T04" + } + } + ] +} \ No newline at end of file diff --git a/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/roles.json b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/roles.json new file mode 100644 index 000000000..adb7c1fa6 --- /dev/null +++ b/libraries/bot-ai-luis-v3/src/test/java/com/microsoft/bot/ai/luis/testdata/roles.json @@ -0,0 +1,2252 @@ +{ + "entities": { + "$instance": { + "a": [ + { + "endIndex": 309, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 299, + "text": "68 degrees", + "type": "builtin.temperature" + } + ], + "arrive": [ + { + "endIndex": 373, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 370, + "text": "5pm", + "type": "builtin.datetimeV2.time" + } + ], + "b": [ + { + "endIndex": 324, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 314, + "text": "72 degrees", + "type": "builtin.temperature" + } + ], + "begin": [ + { + "endIndex": 76, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 65, + "text": "6 years old", + "type": "builtin.age" + } + ], + "buy": [ + { + "endIndex": 124, + "modelType": "Regex Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 119, + "text": "kb922", + "type": "Part" + } + ], + "Buyer": [ + { + "endIndex": 178, + "modelType": "List Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 173, + "text": "delta", + "type": "Airline" + } + ], + "datetime": [ + { + "endIndex": 72, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 65, + "text": "6 years", + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 88, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 81, + "text": "8 years", + "type": "builtin.datetimeV2.duration" + } + ], + "destination": [ + { + "endIndex": 233, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "redmond", + "type": "Weather.Location" + } + ], + "dimension": [ + { + "endIndex": 358, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 355, + "text": "3pm", + "type": "builtin.dimension" + }, + { + "endIndex": 373, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 370, + "text": "5pm", + "type": "builtin.dimension" + } + ], + "end": [ + { + "endIndex": 92, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 81, + "text": "8 years old", + "type": "builtin.age" + } + ], + "geographyV2": [ + { + "endIndex": 218, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "hawaii", + "type": "builtin.geographyV2.state" + }, + { + "endIndex": 233, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "redmond", + "type": "builtin.geographyV2.city" + } + ], + "leave": [ + { + "endIndex": 358, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 355, + "text": "3pm", + "type": "builtin.datetimeV2.time" + } + ], + "length": [ + { + "endIndex": 8, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "3 inches", + "type": "builtin.dimension" + } + ], + "likee": [ + { + "endIndex": 344, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 340, + "text": "mary", + "type": "Name" + } + ], + "liker": [ + { + "endIndex": 333, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 329, + "text": "john", + "type": "Name" + } + ], + "max": [ + { + "endIndex": 403, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 399, + "text": "$500", + "type": "builtin.currency" + } + ], + "maximum": [ + { + "endIndex": 44, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "10%", + "type": "builtin.percentage" + } + ], + "min": [ + { + "endIndex": 394, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 390, + "text": "$400", + "type": "builtin.currency" + } + ], + "minimum": [ + { + "endIndex": 37, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 35, + "text": "5%", + "type": "builtin.percentage" + } + ], + "newPhone": [ + { + "endIndex": 164, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 152, + "text": "206-666-4123", + "type": "builtin.phonenumber" + } + ], + "number": [ + { + "endIndex": 1, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "3", + "type": "builtin.number" + }, + { + "endIndex": 18, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "2", + "type": "builtin.number" + }, + { + "endIndex": 36, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 35, + "text": "5", + "type": "builtin.number" + }, + { + "endIndex": 43, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "10", + "type": "builtin.number" + }, + { + "endIndex": 66, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 65, + "text": "6", + "type": "builtin.number" + }, + { + "endIndex": 82, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 81, + "text": "8", + "type": "builtin.number" + }, + { + "endIndex": 139, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 136, + "text": "425", + "type": "builtin.number" + }, + { + "endIndex": 143, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 140, + "text": "777", + "type": "builtin.number" + }, + { + "endIndex": 148, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 144, + "text": "1212", + "type": "builtin.number" + }, + { + "endIndex": 155, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 152, + "text": "206", + "type": "builtin.number" + }, + { + "endIndex": 159, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "666", + "type": "builtin.number" + }, + { + "endIndex": 164, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 160, + "text": "4123", + "type": "builtin.number" + }, + { + "endIndex": 301, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 299, + "text": "68", + "type": "builtin.number" + }, + { + "endIndex": 316, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 314, + "text": "72", + "type": "builtin.number" + }, + { + "endIndex": 394, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 391, + "text": "400", + "type": "builtin.number" + }, + { + "endIndex": 403, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 400, + "text": "500", + "type": "builtin.number" + } + ], + "old": [ + { + "endIndex": 148, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "score": 0.9, + "startIndex": 136, + "text": "425-777-1212", + "type": "builtin.phonenumber" + } + ], + "oldURL": [ + { + "endIndex": 252, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 238, + "text": "http://foo.com", + "type": "builtin.url" + } + ], + "personName": [ + { + "endIndex": 333, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 329, + "text": "john", + "type": "builtin.personName" + }, + { + "endIndex": 344, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 340, + "text": "mary", + "type": "builtin.personName" + } + ], + "receiver": [ + { + "endIndex": 431, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 413, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "sell": [ + { + "endIndex": 114, + "modelType": "Regex Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 109, + "text": "kb457", + "type": "Part" + } + ], + "Seller": [ + { + "endIndex": 189, + "modelType": "List Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 183, + "text": "virgin", + "type": "Airline" + } + ], + "sender": [ + { + "endIndex": 451, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 437, + "text": "emad@gmail.com", + "type": "builtin.email" + } + ], + "source": [ + { + "endIndex": 218, + "modelType": "Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "hawaii", + "type": "Weather.Location" + } + ], + "url": [ + { + "endIndex": 279, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 264, + "text": "http://blah.com", + "type": "builtin.url" + } + ], + "width": [ + { + "endIndex": 25, + "modelType": "Prebuilt Entity Extractor", + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "2 inches", + "type": "builtin.dimension" + } + ] + }, + "a": [ + { + "number": 68, + "units": "Degree" + } + ], + "arrive": [ + { + "timex": [ + "T17" + ], + "type": "time" + } + ], + "b": [ + { + "number": 72, + "units": "Degree" + } + ], + "begin": [ + { + "number": 6, + "units": "Year" + } + ], + "buy": [ + "kb922" + ], + "Buyer": [ + [ + "Delta" + ] + ], + "datetime": [ + { + "timex": [ + "P6Y" + ], + "type": "duration" + }, + { + "timex": [ + "P8Y" + ], + "type": "duration" + } + ], + "destination": [ + "redmond" + ], + "dimension": [ + { + "number": 3, + "units": "Picometer" + }, + { + "number": 5, + "units": "Picometer" + } + ], + "end": [ + { + "number": 8, + "units": "Year" + } + ], + "geographyV2": [ + { + "location": "hawaii", + "type": "state" + }, + { + "location": "redmond", + "type": "city" + } + ], + "leave": [ + { + "timex": [ + "T15" + ], + "type": "time" + } + ], + "length": [ + { + "number": 3, + "units": "Inch" + } + ], + "likee": [ + "mary" + ], + "liker": [ + "john" + ], + "max": [ + { + "number": 500, + "units": "Dollar" + } + ], + "maximum": [ + 10 + ], + "min": [ + { + "number": 400, + "units": "Dollar" + } + ], + "minimum": [ + 5 + ], + "newPhone": [ + "206-666-4123" + ], + "number": [ + 3, + 2, + 5, + 10, + 6, + 8, + 425, + 777, + 1212, + 206, + 666, + 4123, + 68, + 72, + 400, + 500 + ], + "old": [ + "425-777-1212" + ], + "oldURL": [ + "http://foo.com" + ], + "personName": [ + "john", + "mary" + ], + "receiver": [ + "chrimc@hotmail.com" + ], + "sell": [ + "kb457" + ], + "Seller": [ + [ + "Virgin" + ] + ], + "sender": [ + "emad@gmail.com" + ], + "source": [ + "hawaii" + ], + "url": [ + "http://blah.com" + ], + "width": [ + { + "number": 2, + "units": "Inch" + } + ] + }, + "intents": { + "Cancel": { + "score": 4.48137826E-07 + }, + "Delivery": { + "score": 7.920229E-05 + }, + "EntityTests": { + "score": 0.00420103827 + }, + "Greeting": { + "score": 4.6571725E-07 + }, + "Help": { + "score": 7.5179554E-07 + }, + "None": { + "score": 0.0009303715 + }, + "Roles": { + "score": 1.0 + }, + "search": { + "score": 0.00245359377 + }, + "SpecifyName": { + "score": 5.62756977E-05 + }, + "Travel": { + "score": 0.002153493 + }, + "Weather_GetForecast": { + "score": 0.0100878729 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "text": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com", + "v2": { + "options": { + "IncludeAllIntents": true, + "IncludeInstanceData": true, + "LogPersonalInformation": false, + "Timeout": 100000.0 + }, + "response": { + "entities": [ + { + "endIndex": 332, + "entity": "john", + "role": "liker", + "score": 0.991758049, + "startIndex": 329, + "type": "Name" + }, + { + "endIndex": 343, + "entity": "mary", + "role": "likee", + "score": 0.995282352, + "startIndex": 340, + "type": "Name" + }, + { + "endIndex": 217, + "entity": "hawaii", + "role": "source", + "score": 0.985036731, + "startIndex": 212, + "type": "Weather.Location" + }, + { + "endIndex": 232, + "entity": "redmond", + "role": "destination", + "score": 0.989005446, + "startIndex": 226, + "type": "Weather.Location" + }, + { + "endIndex": 0, + "entity": "3", + "resolution": { + "subtype": "integer", + "value": "3" + }, + "startIndex": 0, + "type": "builtin.number" + }, + { + "endIndex": 17, + "entity": "2", + "resolution": { + "subtype": "integer", + "value": "2" + }, + "startIndex": 17, + "type": "builtin.number" + }, + { + "endIndex": 35, + "entity": "5", + "resolution": { + "subtype": "integer", + "value": "5" + }, + "startIndex": 35, + "type": "builtin.number" + }, + { + "endIndex": 42, + "entity": "10", + "resolution": { + "subtype": "integer", + "value": "10" + }, + "startIndex": 41, + "type": "builtin.number" + }, + { + "endIndex": 65, + "entity": "6", + "resolution": { + "subtype": "integer", + "value": "6" + }, + "startIndex": 65, + "type": "builtin.number" + }, + { + "endIndex": 81, + "entity": "8", + "resolution": { + "subtype": "integer", + "value": "8" + }, + "startIndex": 81, + "type": "builtin.number" + }, + { + "endIndex": 138, + "entity": "425", + "resolution": { + "subtype": "integer", + "value": "425" + }, + "startIndex": 136, + "type": "builtin.number" + }, + { + "endIndex": 142, + "entity": "777", + "resolution": { + "subtype": "integer", + "value": "777" + }, + "startIndex": 140, + "type": "builtin.number" + }, + { + "endIndex": 147, + "entity": "1212", + "resolution": { + "subtype": "integer", + "value": "1212" + }, + "startIndex": 144, + "type": "builtin.number" + }, + { + "endIndex": 154, + "entity": "206", + "resolution": { + "subtype": "integer", + "value": "206" + }, + "startIndex": 152, + "type": "builtin.number" + }, + { + "endIndex": 158, + "entity": "666", + "resolution": { + "subtype": "integer", + "value": "666" + }, + "startIndex": 156, + "type": "builtin.number" + }, + { + "endIndex": 163, + "entity": "4123", + "resolution": { + "subtype": "integer", + "value": "4123" + }, + "startIndex": 160, + "type": "builtin.number" + }, + { + "endIndex": 300, + "entity": "68", + "resolution": { + "subtype": "integer", + "value": "68" + }, + "startIndex": 299, + "type": "builtin.number" + }, + { + "endIndex": 315, + "entity": "72", + "resolution": { + "subtype": "integer", + "value": "72" + }, + "startIndex": 314, + "type": "builtin.number" + }, + { + "endIndex": 393, + "entity": "400", + "resolution": { + "subtype": "integer", + "value": "400" + }, + "startIndex": 391, + "type": "builtin.number" + }, + { + "endIndex": 402, + "entity": "500", + "resolution": { + "subtype": "integer", + "value": "500" + }, + "startIndex": 400, + "type": "builtin.number" + }, + { + "endIndex": 75, + "entity": "6 years old", + "resolution": { + "unit": "Year", + "value": "6" + }, + "role": "begin", + "startIndex": 65, + "type": "builtin.age" + }, + { + "endIndex": 91, + "entity": "8 years old", + "resolution": { + "unit": "Year", + "value": "8" + }, + "role": "end", + "startIndex": 81, + "type": "builtin.age" + }, + { + "endIndex": 71, + "entity": "6 years", + "resolution": { + "values": [ + { + "timex": "P6Y", + "type": "duration", + "value": "189216000" + } + ] + }, + "startIndex": 65, + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 87, + "entity": "8 years", + "resolution": { + "values": [ + { + "timex": "P8Y", + "type": "duration", + "value": "252288000" + } + ] + }, + "startIndex": 81, + "type": "builtin.datetimeV2.duration" + }, + { + "endIndex": 357, + "entity": "3pm", + "resolution": { + "values": [ + { + "timex": "T15", + "type": "time", + "value": "15:00:00" + } + ] + }, + "role": "leave", + "startIndex": 355, + "type": "builtin.datetimeV2.time" + }, + { + "endIndex": 372, + "entity": "5pm", + "resolution": { + "values": [ + { + "timex": "T17", + "type": "time", + "value": "17:00:00" + } + ] + }, + "role": "arrive", + "startIndex": 370, + "type": "builtin.datetimeV2.time" + }, + { + "endIndex": 7, + "entity": "3 inches", + "resolution": { + "unit": "Inch", + "value": "3" + }, + "role": "length", + "startIndex": 0, + "type": "builtin.dimension" + }, + { + "endIndex": 24, + "entity": "2 inches", + "resolution": { + "unit": "Inch", + "value": "2" + }, + "role": "width", + "startIndex": 17, + "type": "builtin.dimension" + }, + { + "endIndex": 357, + "entity": "3pm", + "resolution": { + "unit": "Picometer", + "value": "3" + }, + "startIndex": 355, + "type": "builtin.dimension" + }, + { + "endIndex": 372, + "entity": "5pm", + "resolution": { + "unit": "Picometer", + "value": "5" + }, + "startIndex": 370, + "type": "builtin.dimension" + }, + { + "endIndex": 430, + "entity": "chrimc@hotmail.com", + "resolution": { + "value": "chrimc@hotmail.com" + }, + "role": "receiver", + "startIndex": 413, + "type": "builtin.email" + }, + { + "endIndex": 450, + "entity": "emad@gmail.com", + "resolution": { + "value": "emad@gmail.com" + }, + "role": "sender", + "startIndex": 437, + "type": "builtin.email" + }, + { + "endIndex": 217, + "entity": "hawaii", + "startIndex": 212, + "type": "builtin.geographyV2.state" + }, + { + "endIndex": 232, + "entity": "redmond", + "startIndex": 226, + "type": "builtin.geographyV2.city" + }, + { + "endIndex": 393, + "entity": "$400", + "resolution": { + "unit": "Dollar", + "value": "400" + }, + "role": "min", + "startIndex": 390, + "type": "builtin.currency" + }, + { + "endIndex": 402, + "entity": "$500", + "resolution": { + "unit": "Dollar", + "value": "500" + }, + "role": "max", + "startIndex": 399, + "type": "builtin.currency" + }, + { + "endIndex": 36, + "entity": "5%", + "resolution": { + "value": "5%" + }, + "role": "minimum", + "startIndex": 35, + "type": "builtin.percentage" + }, + { + "endIndex": 43, + "entity": "10%", + "resolution": { + "value": "10%" + }, + "role": "maximum", + "startIndex": 41, + "type": "builtin.percentage" + }, + { + "endIndex": 332, + "entity": "john", + "startIndex": 329, + "type": "builtin.personName" + }, + { + "endIndex": 343, + "entity": "mary", + "startIndex": 340, + "type": "builtin.personName" + }, + { + "endIndex": 147, + "entity": "425-777-1212", + "resolution": { + "score": "0.9", + "value": "425-777-1212" + }, + "role": "old", + "startIndex": 136, + "type": "builtin.phonenumber" + }, + { + "endIndex": 163, + "entity": "206-666-4123", + "resolution": { + "score": "0.9", + "value": "206-666-4123" + }, + "role": "newPhone", + "startIndex": 152, + "type": "builtin.phonenumber" + }, + { + "endIndex": 308, + "entity": "68 degrees", + "resolution": { + "unit": "Degree", + "value": "68" + }, + "role": "a", + "startIndex": 299, + "type": "builtin.temperature" + }, + { + "endIndex": 323, + "entity": "72 degrees", + "resolution": { + "unit": "Degree", + "value": "72" + }, + "role": "b", + "startIndex": 314, + "type": "builtin.temperature" + }, + { + "endIndex": 251, + "entity": "http://foo.com", + "resolution": { + "value": "http://foo.com" + }, + "role": "oldURL", + "startIndex": 238, + "type": "builtin.url" + }, + { + "endIndex": 278, + "entity": "http://blah.com", + "resolution": { + "value": "http://blah.com" + }, + "startIndex": 264, + "type": "builtin.url" + }, + { + "endIndex": 177, + "entity": "delta", + "resolution": { + "values": [ + "Delta" + ] + }, + "role": "Buyer", + "startIndex": 173, + "type": "Airline" + }, + { + "endIndex": 188, + "entity": "virgin", + "resolution": { + "values": [ + "Virgin" + ] + }, + "role": "Seller", + "startIndex": 183, + "type": "Airline" + }, + { + "endIndex": 113, + "entity": "kb457", + "role": "sell", + "startIndex": 109, + "type": "Part" + }, + { + "endIndex": 123, + "entity": "kb922", + "role": "buy", + "startIndex": 119, + "type": "Part" + } + ], + "intents": [ + { + "intent": "Roles", + "score": 1.0 + }, + { + "intent": "Weather.GetForecast", + "score": 0.0100878729 + }, + { + "intent": "EntityTests", + "score": 0.00420103827 + }, + { + "intent": "search", + "score": 0.00245359377 + }, + { + "intent": "Travel", + "score": 0.002153493 + }, + { + "intent": "None", + "score": 0.0009303715 + }, + { + "intent": "Delivery", + "score": 7.920229E-05 + }, + { + "intent": "SpecifyName", + "score": 5.62756977E-05 + }, + { + "intent": "Help", + "score": 7.5179554E-07 + }, + { + "intent": "Greeting", + "score": 4.6571725E-07 + }, + { + "intent": "Cancel", + "score": 4.48137826E-07 + } + ], + "query": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com", + "sentimentAnalysis": { + "label": "neutral", + "score": 0.5 + }, + "topScoringIntent": { + "intent": "Roles", + "score": 1.0 + } + } + }, + "v3": { + "options": { + "IncludeAllIntents": true, + "IncludeAPIResults": true, + "IncludeInstanceData": true, + "Log": true, + "PreferExternalEntities": true, + "Slot": "production" + }, + "response": { + "prediction": { + "entities": { + "$instance": { + "a": [ + { + "length": 10, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "a", + "startIndex": 299, + "text": "68 degrees", + "type": "builtin.temperature" + } + ], + "arrive": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "arrive", + "startIndex": 370, + "text": "5pm", + "type": "builtin.datetimeV2.time" + } + ], + "b": [ + { + "length": 10, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "b", + "startIndex": 314, + "text": "72 degrees", + "type": "builtin.temperature" + } + ], + "begin": [ + { + "length": 11, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "begin", + "startIndex": 65, + "text": "6 years old", + "type": "builtin.age" + } + ], + "buy": [ + { + "length": 5, + "modelType": "Regex Entity Extractor", + "modelTypeId": 8, + "recognitionSources": [ + "model" + ], + "role": "buy", + "startIndex": 119, + "text": "kb922", + "type": "Part" + } + ], + "Buyer": [ + { + "length": 5, + "modelType": "List Entity Extractor", + "modelTypeId": 5, + "recognitionSources": [ + "model" + ], + "role": "Buyer", + "startIndex": 173, + "text": "delta", + "type": "Airline" + } + ], + "datetimeV2": [ + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 65, + "text": "6 years", + "type": "builtin.datetimeV2.duration" + }, + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 81, + "text": "8 years", + "type": "builtin.datetimeV2.duration" + } + ], + "destination": [ + { + "length": 7, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "role": "destination", + "startIndex": 226, + "text": "redmond", + "type": "Weather.Location" + } + ], + "dimension": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 355, + "text": "3pm", + "type": "builtin.dimension" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 370, + "text": "5pm", + "type": "builtin.dimension" + } + ], + "end": [ + { + "length": 11, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "end", + "startIndex": 81, + "text": "8 years old", + "type": "builtin.age" + } + ], + "geographyV2": [ + { + "length": 6, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 212, + "text": "hawaii", + "type": "builtin.geographyV2.state" + }, + { + "length": 7, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 226, + "text": "redmond", + "type": "builtin.geographyV2.city" + } + ], + "leave": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "leave", + "startIndex": 355, + "text": "3pm", + "type": "builtin.datetimeV2.time" + } + ], + "length": [ + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "length", + "startIndex": 0, + "text": "3 inches", + "type": "builtin.dimension" + } + ], + "likee": [ + { + "length": 4, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "role": "likee", + "startIndex": 340, + "text": "mary", + "type": "Name" + } + ], + "liker": [ + { + "length": 4, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "role": "liker", + "startIndex": 329, + "text": "john", + "type": "Name" + } + ], + "max": [ + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "max", + "startIndex": 399, + "text": "$500", + "type": "builtin.currency" + } + ], + "maximum": [ + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "maximum", + "startIndex": 41, + "text": "10%", + "type": "builtin.percentage" + } + ], + "min": [ + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "min", + "startIndex": 390, + "text": "$400", + "type": "builtin.currency" + } + ], + "minimum": [ + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "minimum", + "startIndex": 35, + "text": "5%", + "type": "builtin.percentage" + } + ], + "newPhone": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "newPhone", + "score": 0.9, + "startIndex": 152, + "text": "206-666-4123", + "type": "builtin.phonenumber" + } + ], + "number": [ + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 0, + "text": "3", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 17, + "text": "2", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 35, + "text": "5", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 41, + "text": "10", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 65, + "text": "6", + "type": "builtin.number" + }, + { + "length": 1, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 81, + "text": "8", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 136, + "text": "425", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 140, + "text": "777", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 144, + "text": "1212", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 152, + "text": "206", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 156, + "text": "666", + "type": "builtin.number" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 160, + "text": "4123", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 299, + "text": "68", + "type": "builtin.number" + }, + { + "length": 2, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 314, + "text": "72", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 391, + "text": "400", + "type": "builtin.number" + }, + { + "length": 3, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 400, + "text": "500", + "type": "builtin.number" + } + ], + "old": [ + { + "length": 12, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "old", + "score": 0.9, + "startIndex": 136, + "text": "425-777-1212", + "type": "builtin.phonenumber" + } + ], + "oldURL": [ + { + "length": 14, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "oldURL", + "startIndex": 238, + "text": "http://foo.com", + "type": "builtin.url" + } + ], + "personName": [ + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 329, + "text": "john", + "type": "builtin.personName" + }, + { + "length": 4, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 340, + "text": "mary", + "type": "builtin.personName" + } + ], + "receiver": [ + { + "length": 18, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "receiver", + "startIndex": 413, + "text": "chrimc@hotmail.com", + "type": "builtin.email" + } + ], + "sell": [ + { + "length": 5, + "modelType": "Regex Entity Extractor", + "modelTypeId": 8, + "recognitionSources": [ + "model" + ], + "role": "sell", + "startIndex": 109, + "text": "kb457", + "type": "Part" + } + ], + "Seller": [ + { + "length": 6, + "modelType": "List Entity Extractor", + "modelTypeId": 5, + "recognitionSources": [ + "model" + ], + "role": "Seller", + "startIndex": 183, + "text": "virgin", + "type": "Airline" + } + ], + "sender": [ + { + "length": 14, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "sender", + "startIndex": 437, + "text": "emad@gmail.com", + "type": "builtin.email" + } + ], + "source": [ + { + "length": 6, + "modelType": "Entity Extractor", + "modelTypeId": 1, + "recognitionSources": [ + "model" + ], + "role": "source", + "startIndex": 212, + "text": "hawaii", + "type": "Weather.Location" + } + ], + "url": [ + { + "length": 15, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "startIndex": 264, + "text": "http://blah.com", + "type": "builtin.url" + } + ], + "width": [ + { + "length": 8, + "modelType": "Prebuilt Entity Extractor", + "modelTypeId": 2, + "recognitionSources": [ + "model" + ], + "role": "width", + "startIndex": 17, + "text": "2 inches", + "type": "builtin.dimension" + } + ] + }, + "a": [ + { + "number": 68, + "units": "Degree" + } + ], + "arrive": [ + { + "type": "time", + "values": [ + { + "resolution": [ + { + "value": "17:00:00" + } + ], + "timex": "T17" + } + ] + } + ], + "b": [ + { + "number": 72, + "units": "Degree" + } + ], + "begin": [ + { + "number": 6, + "units": "Year" + } + ], + "buy": [ + "kb922" + ], + "Buyer": [ + [ + "Delta" + ] + ], + "datetimeV2": [ + { + "type": "duration", + "values": [ + { + "resolution": [ + { + "value": "189216000" + } + ], + "timex": "P6Y" + } + ] + }, + { + "type": "duration", + "values": [ + { + "resolution": [ + { + "value": "252288000" + } + ], + "timex": "P8Y" + } + ] + } + ], + "destination": [ + "redmond" + ], + "dimension": [ + { + "number": 3, + "units": "Picometer" + }, + { + "number": 5, + "units": "Picometer" + } + ], + "end": [ + { + "number": 8, + "units": "Year" + } + ], + "geographyV2": [ + { + "type": "state", + "value": "hawaii" + }, + { + "type": "city", + "value": "redmond" + } + ], + "leave": [ + { + "type": "time", + "values": [ + { + "resolution": [ + { + "value": "15:00:00" + } + ], + "timex": "T15" + } + ] + } + ], + "length": [ + { + "number": 3, + "units": "Inch" + } + ], + "likee": [ + "mary" + ], + "liker": [ + "john" + ], + "max": [ + { + "number": 500, + "units": "Dollar" + } + ], + "maximum": [ + 10 + ], + "min": [ + { + "number": 400, + "units": "Dollar" + } + ], + "minimum": [ + 5 + ], + "newPhone": [ + "206-666-4123" + ], + "number": [ + 3, + 2, + 5, + 10, + 6, + 8, + 425, + 777, + 1212, + 206, + 666, + 4123, + 68, + 72, + 400, + 500 + ], + "old": [ + "425-777-1212" + ], + "oldURL": [ + "http://foo.com" + ], + "personName": [ + "john", + "mary" + ], + "receiver": [ + "chrimc@hotmail.com" + ], + "sell": [ + "kb457" + ], + "Seller": [ + [ + "Virgin" + ] + ], + "sender": [ + "emad@gmail.com" + ], + "source": [ + "hawaii" + ], + "url": [ + "http://blah.com" + ], + "width": [ + { + "number": 2, + "units": "Inch" + } + ] + }, + "intents": { + "Cancel": { + "score": 4.48137826E-07 + }, + "Delivery": { + "score": 7.920229E-05 + }, + "EntityTests": { + "score": 0.00420103827 + }, + "Greeting": { + "score": 4.6571725E-07 + }, + "Help": { + "score": 7.5179554E-07 + }, + "None": { + "score": 0.0009303715 + }, + "Roles": { + "score": 1.0 + }, + "search": { + "score": 0.00245359377 + }, + "SpecifyName": { + "score": 5.62756977E-05 + }, + "Travel": { + "score": 0.002153493 + }, + "Weather.GetForecast": { + "score": 0.0100878729 + } + }, + "sentiment": { + "label": "neutral", + "score": 0.5 + }, + "topIntent": "Roles" + }, + "query": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com" + } + } +} diff --git a/doc/README.md b/libraries/bot-ai-qna/README.md similarity index 100% rename from doc/README.md rename to libraries/bot-ai-qna/README.md diff --git a/libraries/bot-ai-qna/pom.xml b/libraries/bot-ai-qna/pom.xml new file mode 100644 index 000000000..fb2612a14 --- /dev/null +++ b/libraries/bot-ai-qna/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-ai-qna + jar + + ${project.groupId}:${project.artifactId} + Bot Framework QnA + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + org.mockito + mockito-core + + + + org.slf4j + slf4j-api + + + + com.microsoft.bot + bot-integration-spring + compile + + + com.microsoft.bot + bot-dialogs + + + com.microsoft.bot + bot-builder + ${project.version} + test-jar + test + + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + mockwebserver + test + + + + + + build + + true + + + + + + + + diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/JoinOperator.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/JoinOperator.java new file mode 100644 index 000000000..cbba1cc6c --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/JoinOperator.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +/** + * Join Operator for Strict Filters. + */ +public enum JoinOperator { + /** + * Default Join Operator, AND. + */ + AND, + + /** + * Join Operator, OR. + */ + OR +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnADialogResponseOptions.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnADialogResponseOptions.java new file mode 100644 index 000000000..be9bcf3ff --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnADialogResponseOptions.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.microsoft.bot.schema.Activity; + +/** + * QnA dialog response options class. + */ +public class QnADialogResponseOptions { + private String activeLearningCardTitle; + private String cardNoMatchText; + private Activity noAnswer; + private Activity cardNoMatchResponse; + + /** + * Gets the active learning card title. + * + * @return The active learning card title + */ + public String getActiveLearningCardTitle() { + return activeLearningCardTitle; + } + + /** + * Sets the active learning card title. + * + * @param withActiveLearningCardTitle The active learning card title. + */ + public void setActiveLearningCardTitle(String withActiveLearningCardTitle) { + this.activeLearningCardTitle = withActiveLearningCardTitle; + } + + /** + * Gets the card no match text. + * + * @return The card no match text. + */ + public String getCardNoMatchText() { + return cardNoMatchText; + } + + /** + * Sets the card no match text. + * + * @param withCardNoMatchText The card no match text. + */ + public void setCardNoMatchText(String withCardNoMatchText) { + this.cardNoMatchText = withCardNoMatchText; + } + + /** + * Gets the no answer activity. + * + * @return The no answer activity. + */ + public Activity getNoAnswer() { + return noAnswer; + } + + /** + * Sets the no answer activity. + * + * @param withNoAnswer The no answer activity. + */ + public void setNoAnswer(Activity withNoAnswer) { + this.noAnswer = withNoAnswer; + } + + /** + * Gets the card no match response. + * + * @return The card no match response. + */ + public Activity getCardNoMatchResponse() { + return cardNoMatchResponse; + } + + /** + * Sets the card no match response. + * + * @param withCardNoMatchResponse The card no match response. + */ + public void setCardNoMatchResponse(Activity withCardNoMatchResponse) { + this.cardNoMatchResponse = withCardNoMatchResponse; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMaker.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMaker.java new file mode 100644 index 000000000..91a233d4e --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMaker.java @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import com.microsoft.bot.ai.qna.models.FeedbackRecords; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.ai.qna.models.QueryResults; +import com.microsoft.bot.ai.qna.utils.ActiveLearningUtils; +import com.microsoft.bot.ai.qna.utils.GenerateAnswerUtils; +import com.microsoft.bot.ai.qna.utils.QnATelemetryConstants; +import com.microsoft.bot.ai.qna.utils.TrainUtils; + +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Pair; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.LoggerFactory; + +/** + * Provides access to a QnA Maker knowledge base. + */ +public class QnAMaker implements QnAMakerClient, TelemetryQnAMaker { + + private QnAMakerEndpoint endpoint; + + private GenerateAnswerUtils generateAnswerHelper; + private TrainUtils activeLearningTrainHelper; + private Boolean logPersonalInformation; + @JsonIgnore + private BotTelemetryClient telemetryClient; + + /** + * The name of the QnAMaker class. + */ + public static final String QNA_MAKER_NAME = "QnAMaker"; + /** + * The type used when logging QnA Maker trace. + */ + public static final String QNA_MAKER_TRACE_TYPE = "https://www.qnamaker.ai/schemas/trace"; + /** + * The label used when logging QnA Maker trace. + */ + public static final String QNA_MAKER_TRACE_LABEL = "QnAMaker Trace"; + + /** + * Initializes a new instance of the QnAMaker class. + * + * @param withEndpoint The endpoint of the knowledge base to + * query. + * @param options The options for the QnA Maker knowledge + * base. + * @param withTelemetryClient The IBotTelemetryClient used for logging + * telemetry events. + * @param withLogPersonalInformation Set to true to include personally + * identifiable information in telemetry + * events. + */ + public QnAMaker( + QnAMakerEndpoint withEndpoint, + QnAMakerOptions options, + BotTelemetryClient withTelemetryClient, + Boolean withLogPersonalInformation + ) { + if (withLogPersonalInformation == null) { + withLogPersonalInformation = false; + } + + if (withEndpoint == null) { + throw new IllegalArgumentException("endpoint"); + } + this.endpoint = withEndpoint; + + if (StringUtils.isBlank(this.endpoint.getKnowledgeBaseId())) { + throw new IllegalArgumentException("knowledgeBaseId"); + } + + if (StringUtils.isBlank(this.endpoint.getHost())) { + throw new IllegalArgumentException("host"); + } + + if (StringUtils.isBlank(this.endpoint.getEndpointKey())) { + throw new IllegalArgumentException("endpointKey"); + } + + if (this.endpoint.getHost().endsWith("v2.0") || this.endpoint.getHost().endsWith("v3.0")) { + throw new UnsupportedOperationException( + "v2.0 and v3.0 of QnA Maker service" + " is no longer supported in the QnA Maker." + ); + } + + this.telemetryClient = withTelemetryClient != null ? withTelemetryClient : new NullBotTelemetryClient(); + this.logPersonalInformation = withLogPersonalInformation; + + this.generateAnswerHelper = new GenerateAnswerUtils(this.endpoint, options); + this.activeLearningTrainHelper = new TrainUtils(this.endpoint); + } + + /** + * Initializes a new instance of the {@link QnAMaker} class. + * + * @param withEndpoint The endpoint of the knowledge base to query. + * @param options The options for the QnA Maker knowledge base. + */ + public QnAMaker(QnAMakerEndpoint withEndpoint, @Nullable QnAMakerOptions options) { + this(withEndpoint, options, null, null); + } + + /** + * Gets a value indicating whether determines whether to log personal + * information that came from the user. + * + * @return If true, will log personal information into the + * IBotTelemetryClient.TrackEvent method; otherwise the properties will + * be filtered. + */ + public Boolean getLogPersonalInformation() { + return this.logPersonalInformation; + } + + /** + * Gets the currently configured {@link BotTelemetryClient}. + * + * @return {@link BotTelemetryClient} being used to log events. + */ + public BotTelemetryClient getTelemetryClient() { + return this.telemetryClient; + } + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question to be + * queried against your knowledge base. + * @param options The options for the QnA Maker knowledge base. If null, + * constructor option is used for this instance. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + public CompletableFuture getAnswers(TurnContext turnContext, @Nullable QnAMakerOptions options) { + return this.getAnswers(turnContext, options, null, null); + } + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question + * to be queried against your knowledge base. + * @param options The options for the QnA Maker knowledge base. If + * null, constructor option is used for this + * instance. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the QnaMessage event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the QnaMessage event. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + public CompletableFuture getAnswers( + TurnContext turnContext, + QnAMakerOptions options, + Map telemetryProperties, + @Nullable Map telemetryMetrics + ) { + return this.getAnswersRaw(turnContext, options, telemetryProperties, telemetryMetrics) + .thenApply(result -> result.getAnswers()); + } + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question + * to be queried against your knowledge base. + * @param options The options for the QnA Maker knowledge base. If + * null, constructor option is used for this + * instance. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the QnaMessage event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the QnaMessage event. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + public CompletableFuture getAnswersRaw( + TurnContext turnContext, + QnAMakerOptions options, + @Nullable Map telemetryProperties, + @Nullable Map telemetryMetrics + ) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException("turnContext")); + } + if (turnContext.getActivity() == null) { + return Async.completeExceptionally( + new IllegalArgumentException( + String.format("The %1$s property for %2$s can't be null.", "Activity", "turnContext") + ) + ); + } + Activity messageActivity = turnContext.getActivity(); + if (messageActivity == null || !messageActivity.isType(ActivityTypes.MESSAGE)) { + return Async.completeExceptionally(new IllegalArgumentException("Activity type is not a message")); + } + + if (StringUtils.isBlank(turnContext.getActivity().getText())) { + return Async.completeExceptionally(new IllegalArgumentException("Null or empty text")); + } + + return this.generateAnswerHelper.getAnswersRaw(turnContext, messageActivity, options).thenCompose(result -> { + try { + this.onQnaResults(result.getAnswers(), turnContext, telemetryProperties, telemetryMetrics); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMaker.class).error("getAnswersRaw"); + } + return CompletableFuture.completedFuture(result); + }); + } + + /** + * Filters the ambiguous question for active learning. + * + * @param queryResult User query output. + * @return Filtered array of ambiguous question. + */ + public QueryResult[] getLowScoreVariation(QueryResult[] queryResult) { + List queryResults = ActiveLearningUtils.getLowScoreVariation(Arrays.asList(queryResult)); + return queryResults.toArray(new QueryResult[queryResults.size()]); + } + + /** + * Send feedback to the knowledge base. + * + * @param feedbackRecords Feedback records. + * @return Representing the asynchronous operation. + * @throws IOException Throws an IOException if there is any. + */ + public CompletableFuture callTrain(FeedbackRecords feedbackRecords) throws IOException { + return this.activeLearningTrainHelper.callTrain(feedbackRecords); + } + + /** + * Executed when a result is returned from QnA Maker. + * + * @param queryResults An array of {@link QueryResult} + * @param turnContext The {@link TurnContext} + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return A Task representing the work to be executed. + * @throws IOException Throws an IOException if there is any. + */ + protected CompletableFuture onQnaResults( + QueryResult[] queryResults, + TurnContext turnContext, + @Nullable Map telemetryProperties, + @Nullable Map telemetryMetrics + ) throws IOException { + return fillQnAEvent(queryResults, turnContext, telemetryProperties, telemetryMetrics).thenAccept( + eventData -> { + // Track the event + this.telemetryClient + .trackEvent(QnATelemetryConstants.QNA_MSG_EVENT, eventData.getLeft(), eventData.getRight()); + } + ); + } + + /** + * Fills the event properties and metrics for the QnaMessage event for + * telemetry. These properties are logged when the QnA GetAnswers method is + * called. + * + * @param queryResults QnA service results. + * @param turnContext Context object containing information for a single + * turn of conversation with a user. + * @param telemetryProperties Properties to add/override for the event. + * @param telemetryMetrics Metrics to add/override for the event. + * @return A tuple of Properties and Metrics that will be sent to the + * IBotTelemetryClient. TrackEvent method for the QnAMessage event. The + * properties and metrics returned the standard properties logged with + * any properties passed from the GetAnswersAsync method. + * @throws IOException Throws an IOException if there is any. + */ + protected CompletableFuture, Map>> fillQnAEvent( + QueryResult[] queryResults, + TurnContext turnContext, + @Nullable Map telemetryProperties, + @Nullable Map telemetryMetrics + ) throws IOException { + Map properties = new HashMap(); + Map metrics = new HashMap(); + + properties.put(QnATelemetryConstants.KNOWLEDGE_BASE_ID_PROPERTY, this.endpoint.getKnowledgeBaseId()); + + String text = turnContext.getActivity().getText(); + String userName = + turnContext.getActivity().getFrom() != null ? turnContext.getActivity().getFrom().getName() : null; + + // Use the LogPersonalInformation flag to toggle logging PII data, text and user + // name are common examples + if (this.logPersonalInformation) { + if (!StringUtils.isBlank(text)) { + properties.put(QnATelemetryConstants.QUESTION_PROPERTY, text); + } + + if (!StringUtils.isBlank(userName)) { + properties.put(QnATelemetryConstants.USERNAME_PROPERTY, userName); + } + } + + // Fill in QnA Results (found or not) + if (queryResults.length > 0) { + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + QueryResult queryResult = queryResults[0]; + properties.put( + QnATelemetryConstants.MATCHED_QUESTION_PROPERTY, + jacksonAdapter.serialize(queryResult.getQuestions()) + ); + properties.put( + QnATelemetryConstants.QUESTION_ID_PROPERTY, + queryResult.getId() != null ? queryResult.getId().toString() : "" + ); + properties.put(QnATelemetryConstants.ANSWER_PROPERTY, queryResult.getAnswer()); + metrics.put(QnATelemetryConstants.SCORE_PROPERTY, queryResult.getScore().doubleValue()); + properties.put(QnATelemetryConstants.ARTICLE_FOUND_PROPERTY, "true"); + } else { + properties.put(QnATelemetryConstants.MATCHED_QUESTION_PROPERTY, "No Qna Question matched"); + properties.put(QnATelemetryConstants.QUESTION_ID_PROPERTY, "No QnA Question Id matched"); + properties.put(QnATelemetryConstants.ANSWER_PROPERTY, "No Qna Answer matched"); + properties.put(QnATelemetryConstants.ARTICLE_FOUND_PROPERTY, "false"); + } + + // Additional Properties can override "stock" properties. + if (telemetryProperties != null) { + Multimap multiMapTelemetryProperties = LinkedListMultimap.create(); + for (Entry entry : telemetryProperties.entrySet()) { + multiMapTelemetryProperties.put(entry.getKey(), entry.getValue()); + } + for (Entry entry : properties.entrySet()) { + multiMapTelemetryProperties.put(entry.getKey(), entry.getValue()); + } + for (Entry> entry : multiMapTelemetryProperties.asMap().entrySet()) { + telemetryProperties.put(entry.getKey(), entry.getValue().iterator().next()); + } + } + + // Additional Metrics can override "stock" metrics. + if (telemetryMetrics != null) { + Multimap multiMapTelemetryMetrics = LinkedListMultimap.create(); + for (Entry entry : telemetryMetrics.entrySet()) { + multiMapTelemetryMetrics.put(entry.getKey(), entry.getValue()); + } + for (Entry entry : metrics.entrySet()) { + multiMapTelemetryMetrics.put(entry.getKey(), entry.getValue()); + } + for (Entry> entry : multiMapTelemetryMetrics.asMap().entrySet()) { + telemetryMetrics.put(entry.getKey(), entry.getValue().iterator().next()); + } + } + + Map telemetryPropertiesResult = telemetryProperties != null ? telemetryProperties : properties; + Map telemetryMetricsResult = telemetryMetrics != null ? telemetryMetrics : metrics; + return CompletableFuture.completedFuture(new Pair<>(telemetryPropertiesResult, telemetryMetricsResult)); + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerClient.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerClient.java new file mode 100644 index 000000000..e2299745c --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerClient.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +import com.microsoft.bot.ai.qna.models.FeedbackRecords; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.ai.qna.models.QueryResults; +import com.microsoft.bot.builder.TurnContext; + +/** + * Client to access a QnA Maker knowledge base. + */ +public interface QnAMakerClient { + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question + * to be queried against your knowledge base. + * @param options The options for the QnA Maker knowledge base. If + * null, constructor option is used for this + * instance. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the QnaMessage event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the QnaMessage event. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + CompletableFuture getAnswers( + TurnContext turnContext, + QnAMakerOptions options, + Map telemetryProperties, + @Nullable Map telemetryMetrics + ); + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question + * to be queried against your knowledge base. + * @param options The options for the QnA Maker knowledge base. If + * null, constructor option is used for this + * instance. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the QnaMessage event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the QnaMessage event. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + CompletableFuture getAnswersRaw( + TurnContext turnContext, + QnAMakerOptions options, + @Nullable Map telemetryProperties, + @Nullable Map telemetryMetrics + ); + + /** + * Filters the ambiguous question for active learning. + * + * @param queryResults User query output. + * @return Filtered array of ambiguous question. + */ + QueryResult[] getLowScoreVariation(QueryResult[] queryResults); + + /** + * Send feedback to the knowledge base. + * + * @param feedbackRecords Feedback records. + * @return A Task representing the asynchronous operation. + * @throws IOException Throws an IOException if there is any. + */ + CompletableFuture callTrain(FeedbackRecords feedbackRecords) throws IOException; +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerEndpoint.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerEndpoint.java new file mode 100644 index 000000000..c185124b7 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerEndpoint.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Defines an endpoint used to connect to a QnA Maker Knowledge base. + */ +public class QnAMakerEndpoint { + @JsonProperty("knowledgeBaseId") + private String knowledgeBaseId; + + @JsonProperty("endpointKey") + private String endpointKey; + + @JsonProperty("host") + private String host; + + /** + * Gets the knowledge base ID. + * + * @return The knowledge base ID. + */ + public String getKnowledgeBaseId() { + return knowledgeBaseId; + } + + /** + * Sets the knowledge base ID. + * + * @param withKnowledgeBaseId The knowledge base ID. + */ + public void setKnowledgeBaseId(String withKnowledgeBaseId) { + this.knowledgeBaseId = withKnowledgeBaseId; + } + + /** + * Gets the endpoint key for the knowledge base. + * + * @return The endpoint key for the knowledge base. + */ + public String getEndpointKey() { + return endpointKey; + } + + /** + * Sets the endpoint key for the knowledge base. + * + * @param withEndpointKey The endpoint key for the knowledge base. + */ + public void setEndpointKey(String withEndpointKey) { + this.endpointKey = withEndpointKey; + } + + /** + * Gets the host path. For example + * "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0". + * + * @return The host path. + */ + public String getHost() { + return host; + } + + /** + * Sets the host path. For example + * "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0". + * + * @param withHost The host path. + */ + public void setHost(String withHost) { + this.host = withHost; + } + + /** + * Initializes a new instance of the {@link QnAMakerEndpoint} class. + */ + public QnAMakerEndpoint() { + + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerOptions.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerOptions.java new file mode 100644 index 000000000..d1d91e3d7 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerOptions.java @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.ai.qna.models.Metadata; +import com.microsoft.bot.ai.qna.models.QnARequestContext; + +/** + * Defines options for the QnA Maker knowledge base. + */ +public class QnAMakerOptions { + @JsonProperty("scoreThreshold") + private Float scoreThreshold; + + @JsonProperty("timeout") + private Double timeout = 0d; + + @JsonProperty("top") + private Integer top = 0; + + @JsonProperty("context") + private QnARequestContext context; + + @JsonProperty("qnAId") + private Integer qnAId; + + @JsonProperty("strictFilters") + private Metadata[] strictFilters; + + @Deprecated + @JsonIgnore + private Metadata[] metadataBoost; + + @JsonProperty("isTest") + private Boolean isTest; + + @JsonProperty("rankerType") + private String rankerType; + + @JsonProperty("strictFiltersJoinOperator") + private JoinOperator strictFiltersJoinOperator; + + private static final Float SCORE_THRESHOLD = 0.3f; + + /** + * Gets the minimum score threshold, used to filter returned results. Scores are + * normalized to the range of 0.0 to 1.0 before filtering. + * + * @return The minimum score threshold, used to filter returned results. + */ + public Float getScoreThreshold() { + return scoreThreshold; + } + + /** + * Sets the minimum score threshold, used to filter returned results. Scores are + * normalized to the range of 0.0 to 1.0 before filtering. + * + * @param withScoreThreshold The minimum score threshold, used to filter + * returned results. + */ + public void setScoreThreshold(Float withScoreThreshold) { + this.scoreThreshold = withScoreThreshold; + } + + /** + * Gets the time in milliseconds to wait before the request times out. + * + * @return The time in milliseconds to wait before the request times out. + * Default is 100000 milliseconds. This property allows users to set + * Timeout without having to pass in a custom HttpClient to QnAMaker + * class constructor. If using custom HttpClient, then set Timeout value + * in HttpClient instead of QnAMakerOptions.Timeout. + */ + public Double getTimeout() { + return timeout; + } + + /** + * Sets the time in milliseconds to wait before the request times out. + * + * @param withTimeout The time in milliseconds to wait before the request times + * out. Default is 100000 milliseconds. This property allows + * users to set Timeout without having to pass in a custom + * HttpClient to QnAMaker class constructor. If using custom + * HttpClient, then set Timeout value in HttpClient instead + * of QnAMakerOptions.Timeout. + */ + public void setTimeout(Double withTimeout) { + this.timeout = withTimeout; + } + + /** + * Gets the number of ranked results you want in the output. + * + * @return The number of ranked results you want in the output. + */ + public Integer getTop() { + return top; + } + + /** + * Sets the number of ranked results you want in the output. + * + * @param withTop The number of ranked results you want in the output. + */ + public void setTop(Integer withTop) { + this.top = withTop; + } + + /** + * Gets context of the previous turn. + * + * @return The context of previous turn. + */ + public QnARequestContext getContext() { + return context; + } + + /** + * Sets context of the previous turn. + * + * @param withContext The context of previous turn. + */ + public void setContext(QnARequestContext withContext) { + this.context = withContext; + } + + /** + * Gets QnA Id of the current question asked (if availble). + * + * @return Id of the current question asked. + */ + public Integer getQnAId() { + return qnAId; + } + + /** + * Sets QnA Id of the current question asked (if availble). + * + * @param withQnAId Id of the current question asked. + */ + public void setQnAId(Integer withQnAId) { + this.qnAId = withQnAId; + } + + /** + * Gets the {@link Metadata} collection to be sent when calling QnA Maker to + * filter results. + * + * @return An array of {@link Metadata} + */ + public Metadata[] getStrictFilters() { + return strictFilters; + } + + /** + * Sets the {@link Metadata} collection to be sent when calling QnA Maker to + * filter results. + * + * @param withStrictFilters An array of {@link Metadata} + */ + public void setStrictFilters(Metadata[] withStrictFilters) { + this.strictFilters = withStrictFilters; + } + + /** + * Gets a value indicating whether to call test or prod environment of knowledge + * base to be called. + * + * @return A value indicating whether to call test or prod environment of + * knowledge base. + */ + public Boolean getIsTest() { + return isTest; + } + + /** + * Sets a value indicating whether to call test or prod environment of knowledge + * base to be called. + * + * @param withIsTest A value indicating whether to call test or prod environment + * of knowledge base. + */ + public void setIsTest(Boolean withIsTest) { + isTest = withIsTest; + } + + /** + * Gets the QnA Maker ranker type to use. + * + * @return The QnA Maker ranker type to use. + */ + public String getRankerType() { + return rankerType; + } + + /** + * Sets the QnA Maker ranker type to use. + * + * @param withRankerType The QnA Maker ranker type to use. + */ + public void setRankerType(String withRankerType) { + this.rankerType = withRankerType; + } + + /** + * Gets Strict Filters join operator. + * + * @return A value indicating choice for Strict Filters Join Operation. + */ + public JoinOperator getStrictFiltersJoinOperator() { + return strictFiltersJoinOperator; + } + + /** + * Sets Strict Filters join operator. + * + * @param withStrictFiltersJoinOperator A value indicating choice for Strict + * Filters Join Operation. + */ + public void setStrictFiltersJoinOperator(JoinOperator withStrictFiltersJoinOperator) { + this.strictFiltersJoinOperator = withStrictFiltersJoinOperator; + } + + /** + * Initializes a new instance of the {@link QnAMakerOptions} class. + */ + public QnAMakerOptions() { + this.scoreThreshold = SCORE_THRESHOLD; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerRecognizer.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerRecognizer.java new file mode 100644 index 000000000..1414445f6 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerRecognizer.java @@ -0,0 +1,554 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import com.microsoft.bot.ai.qna.models.Metadata; +import com.microsoft.bot.ai.qna.models.QnARequestContext; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.ai.qna.models.RankerTypes; +import com.microsoft.bot.builder.IntentScore; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.Recognizer; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Serialization; + +/** + * IRecognizer implementation which uses QnAMaker KB to identify intents. + */ +public class QnAMakerRecognizer extends Recognizer { + + private static final Integer TOP_DEFAULT_VALUE = 3; + private static final Float THRESHOLD_DEFAULT_VALUE = 0.3f; + + private final String qnAMatchIntent = "QnAMatch"; + + private final String intentPrefix = "intent="; + + @JsonProperty("knowledgeBaseId") + private String knowledgeBaseId; + + @JsonProperty("hostname") + private String hostName; + + @JsonProperty("endpointKey") + private String endpointKey; + + @JsonProperty("top") + private Integer top = TOP_DEFAULT_VALUE; + + @JsonProperty("threshold") + private Float threshold = THRESHOLD_DEFAULT_VALUE; + + @JsonProperty("isTest") + private Boolean isTest; + + @JsonProperty("rankerType") + private String rankerType = RankerTypes.DEFAULT_RANKER_TYPE; + + @JsonProperty("strictFiltersJoinOperator") + private JoinOperator strictFiltersJoinOperator; + + @JsonProperty("includeDialogNameInMetadata") + private Boolean includeDialogNameInMetadata = true; + + @JsonProperty("metadata") + private Metadata[] metadata; + + @JsonProperty("context") + private QnARequestContext context; + + @JsonProperty("qnaId") + private Integer qnAId = 0; + + @JsonProperty("logPersonalInformation") + private Boolean logPersonalInformation = false; + + /** + * Gets key used when adding the intent to the {@link RecognizerResult} intents + * collection. + * + * @return Key used when adding the intent to the {@link RecognizerResult} + * intents collection. + */ + public String getQnAMatchIntent() { + return qnAMatchIntent; + } + + /** + * Gets the KnowledgeBase Id of your QnA Maker KnowledgeBase. + * + * @return The knowledgebase Id. + */ + public String getKnowledgeBaseId() { + return knowledgeBaseId; + } + + /** + * Sets the KnowledgeBase Id of your QnA Maker KnowledgeBase. + * + * @param withKnowledgeBaseId The knowledgebase Id. + */ + public void setKnowledgeBaseId(String withKnowledgeBaseId) { + this.knowledgeBaseId = withKnowledgeBaseId; + } + + /** + * Gets the Hostname for your QnA Maker service. + * + * @return The host name of the QnA Maker knowledgebase. + */ + public String getHostName() { + return hostName; + } + + /** + * Sets the Hostname for your QnA Maker service. + * + * @param withHostName The host name of the QnA Maker knowledgebase. + */ + public void setHostName(String withHostName) { + this.hostName = withHostName; + } + + /** + * Gets the Endpoint key for the QnA Maker KB. + * + * @return The endpoint key for the QnA service. + */ + public String getEndpointKey() { + return endpointKey; + } + + /** + * Sets the Endpoint key for the QnA Maker KB. + * + * @param withEndpointKey The endpoint key for the QnA service. + */ + public void setEndpointKey(String withEndpointKey) { + this.endpointKey = withEndpointKey; + } + + /** + * Gets the number of results you want. + * + * @return The number of results you want. + */ + public Integer getTop() { + return top; + } + + /** + * Sets the number of results you want. + * + * @param withTop The number of results you want. + */ + public void setTop(Integer withTop) { + this.top = withTop; + } + + /** + * Gets the threshold score to filter results. + * + * @return The threshold for the results. + */ + public Float getThreshold() { + return threshold; + } + + /** + * Sets the threshold score to filter results. + * + * @param withThreshold The threshold for the results. + */ + public void setThreshold(Float withThreshold) { + this.threshold = withThreshold; + } + + /** + * Gets a value indicating whether gets or sets environment of knowledgebase to + * be called. + * + * @return A value indicating whether to call test or prod environment of + * knowledgebase. + */ + public Boolean getIsTest() { + return isTest; + } + + /** + * Sets a value indicating whether gets or sets environment of knowledgebase to + * be called. + * + * @param withIsTest A value indicating whether to call test or prod environment + * of knowledgebase. + */ + public void setIsTest(Boolean withIsTest) { + this.isTest = withIsTest; + } + + /** + * Gets ranker Type. + * + * @return The desired RankerType. + */ + public String getRankerType() { + return rankerType; + } + + /** + * Sets ranker Type. + * + * @param withRankerType The desired RankerType. + */ + public void setRankerType(String withRankerType) { + this.rankerType = withRankerType; + } + + /** + * Gets {@link Metadata} join operator. + * + * @return A value used for Join operation of Metadata . + */ + public JoinOperator getStrictFiltersJoinOperator() { + return strictFiltersJoinOperator; + } + + /** + * Sets {@link Metadata} join operator. + * + * @param withStrictFiltersJoinOperator A value used for Join operation of + * Metadata {@link Metadata}. + */ + public void setStrictFiltersJoinOperator(JoinOperator withStrictFiltersJoinOperator) { + this.strictFiltersJoinOperator = withStrictFiltersJoinOperator; + } + + /** + * Gets the whether to include the dialog name metadata for QnA context. + * + * @return A bool or boolean expression. + */ + public Boolean getIncludeDialogNameInMetadata() { + return includeDialogNameInMetadata; + } + + /** + * Sets the whether to include the dialog name metadata for QnA context. + * + * @param withIncludeDialogNameInMetadata A bool or boolean expression. + */ + public void setIncludeDialogNameInMetadata(Boolean withIncludeDialogNameInMetadata) { + this.includeDialogNameInMetadata = withIncludeDialogNameInMetadata; + } + + /** + * Gets an expression to evaluate to set additional metadata name value pairs. + * + * @return An expression to evaluate for pairs of metadata. + */ + public Metadata[] getMetadata() { + return metadata; + } + + /** + * Sets an expression to evaluate to set additional metadata name value pairs. + * + * @param withMetadata An expression to evaluate for pairs of metadata. + */ + public void setMetadata(Metadata[] withMetadata) { + this.metadata = withMetadata; + } + + /** + * Gets an expression to evaluate to set the context. + * + * @return An expression to evaluate to QnARequestContext to pass as context. + */ + public QnARequestContext getContext() { + return context; + } + + /** + * Sets an expression to evaluate to set the context. + * + * @param withContext An expression to evaluate to QnARequestContext to pass as + * context. + */ + public void setContext(QnARequestContext withContext) { + this.context = withContext; + } + + /** + * Gets an expression or numberto use for the QnAId paratemer. + * + * @return The expression or number. + */ + public Integer getQnAId() { + return qnAId; + } + + /** + * Sets an expression or numberto use for the QnAId paratemer. + * + * @param withQnAId The expression or number. + */ + public void setQnAId(Integer withQnAId) { + this.qnAId = withQnAId; + } + + /** + * Gets the flag to determine if personal information should be logged in + * telemetry. + * + * @return The flag to indicate in personal information should be logged in + * telemetry. + */ + public Boolean getLogPersonalInformation() { + return logPersonalInformation; + } + + /** + * Sets the flag to determine if personal information should be logged in + * telemetry. + * + * @param withLogPersonalInformation The flag to indicate in personal + * information should be logged in telemetry. + */ + public void setLogPersonalInformation(Boolean withLogPersonalInformation) { + this.logPersonalInformation = withLogPersonalInformation; + } + + /** + * Return results of the call to QnA Maker. + * + * @param dialogContext Context object containing information for a single + * turn of conversation with a user. + * @param activity The incoming activity received from the user. The + * Text property value is used as the query text for + * QnA Maker. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the LuisResult event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the LuisResult event. + * @return A {@link RecognizerResult} containing the QnA Maker result. + */ + @Override + public CompletableFuture recognize( + DialogContext dialogContext, + Activity activity, + Map telemetryProperties, + Map telemetryMetrics + ) { + // Identify matched intents + RecognizerResult recognizerResult = new RecognizerResult(); + recognizerResult.setText(activity.getText()); + recognizerResult.setIntents(new HashMap()); + if (Strings.isNullOrEmpty(activity.getText())) { + recognizerResult.getIntents().put("None", new IntentScore()); + return CompletableFuture.completedFuture(recognizerResult); + } + + List filters = new ArrayList(); + // TODO this should be uncommented as soon as Expression is added in Java + /* + * if (this.includeDialogNameInMetadata.getValue(dialogContext.getState())) { + * filters.add(new Metadata() { { setName("dialogName"); + * setValue(dialogContext.getActiveDialog().getId()); } }); } + */ + + // if there is $qna.metadata set add to filters + Metadata[] externalMetadata = this.metadata; + if (externalMetadata != null) { + filters.addAll(Arrays.asList(externalMetadata)); + } + + QnAMakerOptions options = new QnAMakerOptions(); + options.setContext(context); + options.setScoreThreshold(threshold); + options.setStrictFilters(filters.toArray(new Metadata[filters.size()])); + options.setTop(top); + options.setQnAId(qnAId); + options.setIsTest(isTest); + options.setStrictFiltersJoinOperator(strictFiltersJoinOperator); + + // Calling QnAMaker to get response. + return this.getQnAMakerClient(dialogContext).thenCompose(qnaClient -> { + return qnaClient.getAnswers(dialogContext.getContext(), options, null, null).thenApply(answers -> { + if (answers.length > 0) { + QueryResult topAnswer = null; + for (QueryResult answer : answers) { + if (topAnswer == null || answer.getScore() > topAnswer.getScore()) { + topAnswer = answer; + } + } + Float internalTopAnswer = topAnswer.getScore(); + if (topAnswer.getAnswer().trim().toUpperCase().startsWith(intentPrefix.toUpperCase())) { + IntentScore intentScore = new IntentScore(); + intentScore.setScore(internalTopAnswer); + recognizerResult.getIntents() + .put(topAnswer.getAnswer().trim().substring( + intentPrefix.length()).trim(), + intentScore + ); + } else { + IntentScore intentScore = new IntentScore(); + intentScore.setScore(internalTopAnswer); + recognizerResult.getIntents().put(this.qnAMatchIntent, intentScore); + } + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectNode entitiesNode = mapper.createObjectNode(); + List answerArray = new ArrayList(); + answerArray.add(topAnswer.getAnswer()); + ArrayNode entitiesArrayNode = entitiesNode.putArray("answer"); + entitiesArrayNode.add(topAnswer.getAnswer()); + + ObjectNode instance = entitiesNode.putObject("$instance"); + ArrayNode instanceArrayNode = instance.putArray("answer"); + ObjectNode data = instanceArrayNode.addObject(); + data.setAll((ObjectNode) mapper.valueToTree(topAnswer)); + data.put("startIndex", 0); + data.put("endIndex", activity.getText().length()); + + recognizerResult.setEntities(entitiesNode); + recognizerResult.getProperties().put("answers", mapper.valueToTree(answers)); + } else { + IntentScore intentScore = new IntentScore(); + intentScore.setScore(1.0f); + recognizerResult.getIntents().put("None", intentScore); + } + + this.trackRecognizerResult( + dialogContext, + "QnAMakerRecognizerResult", + this.fillRecognizerResultTelemetryProperties(recognizerResult, telemetryProperties, dialogContext), + telemetryMetrics + ); + return recognizerResult; + }); + }); + } + + /** + * Gets an instance of {@link QnAMakerClient}. + * + * @param dc The {@link DialogContext} used to access state. + * @return An instance of {@link QnAMakerClient}. + */ + protected CompletableFuture getQnAMakerClient(DialogContext dc) { + QnAMakerClient qnaClient = dc.getContext().getTurnState().get(QnAMakerClient.class); + if (qnaClient != null) { + // return mock client + return CompletableFuture.completedFuture(qnaClient); + } + + String epKey = this.endpointKey; + String hn = this.hostName; + String kbId = this.knowledgeBaseId; + Boolean logPersonalInfo = this.logPersonalInformation; + QnAMakerEndpoint endpoint = new QnAMakerEndpoint(); + endpoint.setEndpointKey(epKey); + endpoint.setHost(hn); + endpoint.setKnowledgeBaseId(kbId); + + return CompletableFuture + .completedFuture(new QnAMaker(endpoint, new QnAMakerOptions(), this.getTelemetryClient(), logPersonalInfo)); + } + + /** + * Uses the RecognizerResult to create a list of properties to be included when + * tracking the result in telemetry. + * + * @param recognizerResult Recognizer Result. + * @param telemetryProperties A list of properties to append or override the + * properties created using the RecognizerResult. + * @param dialogContext Dialog Context. + * @return A dictionary that can be included when calling the TrackEvent method + * on the TelemetryClient. + */ + @Override + protected Map fillRecognizerResultTelemetryProperties( + RecognizerResult recognizerResult, + Map telemetryProperties, + @Nullable DialogContext dialogContext + ) { + if (dialogContext == null) { + throw new IllegalArgumentException( + "dialogContext: DialogContext needed for state in " + + "AdaptiveRecognizer.FillRecognizerResultTelemetryProperties method." + ); + } + + Map properties = new HashMap(); + properties.put( + "TopIntent", + !recognizerResult.getIntents().isEmpty() + ? (String) recognizerResult.getIntents().keySet().toArray()[0] + : null + ); + properties.put( + "TopIntentScore", + !recognizerResult.getIntents() + .isEmpty() + ? Double.toString( + ((IntentScore) recognizerResult.getIntents().values().toArray()[0]).getScore() + ) + : null + ); + properties.put( + "Intents", + !recognizerResult.getIntents().isEmpty() + ? Serialization.toStringSilent(recognizerResult.getIntents()) + : null + ); + properties.put( + "Entities", + recognizerResult.getEntities() != null + ? Serialization.toStringSilent(recognizerResult.getEntities()) + : null + ); + properties.put( + "AdditionalProperties", + !recognizerResult.getProperties().isEmpty() + ? Serialization.toStringSilent(recognizerResult.getProperties()) + : null + ); + if (logPersonalInformation && !Strings.isNullOrEmpty(recognizerResult.getText())) { + properties.put("Text", recognizerResult.getText()); + properties.put("AlteredText", recognizerResult.getAlteredText()); + } + + // Additional Properties can override "stock properties". + if (telemetryProperties != null) { + telemetryProperties.putAll(properties); + Map> telemetryPropertiesMap = + telemetryProperties.entrySet() + .stream() + .collect( + Collectors.groupingBy(Entry::getKey, Collectors.mapping(Entry::getValue, Collectors.toList())) + ); + return telemetryPropertiesMap.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().get(0))); + } + + return properties; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/TelemetryQnAMaker.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/TelemetryQnAMaker.java new file mode 100644 index 000000000..3ad30ff83 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/TelemetryQnAMaker.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.TurnContext; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +/** + * Interface for adding telemetry logging capabilities to {@link QnAMaker}/>. + */ +public interface TelemetryQnAMaker { + + /** + * Gets a value indicating whether determines whether to log personal + * information that came from the user. + * + * @return If true, will log personal information into the + * IBotTelemetryClient.TrackEvent method; otherwise the properties will + * be filtered. + */ + Boolean getLogPersonalInformation(); + + /** + * Gets the currently configured {@link BotTelemetryClient} that logs the + * QnaMessage event. + * + * @return The {@link BotTelemetryClient} being used to log events. + */ + BotTelemetryClient getTelemetryClient(); + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question + * to be queried against your knowledge base. + * @param options The options for the QnA Maker knowledge base. If + * null, constructor option is used for this + * instance. + * @param telemetryProperties Additional properties to be logged to telemetry + * with the QnaMessage event. + * @param telemetryMetrics Additional metrics to be logged to telemetry with + * the QnaMessage event. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + CompletableFuture getAnswers( + TurnContext turnContext, + QnAMakerOptions options, + Map telemetryProperties, + @Nullable Map telemetryMetrics + ); +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialog.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialog.java new file mode 100644 index 000000000..606caef8a --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialog.java @@ -0,0 +1,1015 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.dialogs; + +import com.microsoft.bot.connector.Async; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.ai.qna.QnADialogResponseOptions; +import com.microsoft.bot.ai.qna.QnAMaker; +import com.microsoft.bot.ai.qna.QnAMakerClient; +import com.microsoft.bot.ai.qna.QnAMakerEndpoint; +import com.microsoft.bot.ai.qna.QnAMakerOptions; +import com.microsoft.bot.ai.qna.models.FeedbackRecord; +import com.microsoft.bot.ai.qna.models.FeedbackRecords; +import com.microsoft.bot.ai.qna.models.Metadata; +import com.microsoft.bot.ai.qna.models.QnARequestContext; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.ai.qna.models.QueryResults; +import com.microsoft.bot.ai.qna.models.RankerTypes; +import com.microsoft.bot.ai.qna.utils.ActiveLearningUtils; +import com.microsoft.bot.ai.qna.utils.BindToActivity; +import com.microsoft.bot.ai.qna.utils.QnACardBuilder; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogEvent; +import com.microsoft.bot.dialogs.DialogReason; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.DialogTurnStatus; +import com.microsoft.bot.dialogs.ObjectPath; +import com.microsoft.bot.dialogs.TurnPath; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; + +import okhttp3.OkHttpClient; +import org.slf4j.LoggerFactory; + +/** + * A dialog that supports multi-step and adaptive-learning QnA Maker services. + * An instance of this class targets a specific QnA Maker knowledge base. It + * supports knowledge bases that include follow-up prompt and active learning + * features. + */ +public class QnAMakerDialog extends WaterfallDialog { + + @JsonIgnore + private OkHttpClient httpClient; + + @JsonProperty("knowledgeBaseId") + private String knowledgeBaseId; + + @JsonProperty("hostName") + private String hostName; + + @JsonProperty("endpointKey") + private String endpointKey; + + @JsonProperty("threshold") + private Float threshold = DEFAULT_THRESHOLD; + + @JsonProperty("top") + private Integer top = DEFAULT_TOP_N; + + @JsonProperty("noAnswer") + private BindToActivity noAnswer = new BindToActivity(MessageFactory.text(DEFAULT_NO_ANSWER)); + + @JsonProperty("activeLearningCardTitle") + private String activeLearningCardTitle; + + @JsonProperty("cardNoMatchText") + private String cardNoMatchText; + + @JsonProperty("cardNoMatchResponse") + private BindToActivity cardNoMatchResponse = + new BindToActivity(MessageFactory.text(DEFAULT_CARD_NO_MATCH_RESPONSE)); + + @JsonProperty("strictFilters") + private Metadata[] strictFilters; + + @JsonProperty("logPersonalInformation") + private Boolean logPersonalInformation = false; + + @JsonProperty("isTest") + private Boolean isTest; + + @JsonProperty("rankerType") + private String rankerType = RankerTypes.DEFAULT_RANKER_TYPE; + + /** + * The path for storing and retrieving QnA Maker context data. This represents + * context about the current or previous call to QnA Maker. It is stored within + * the current step's {@link WaterfallStepContext}. It supports QnA Maker's + * follow-up prompt and active learning features. + */ + protected static final String QNA_CONTEXT_DATA = "qnaContextData"; + + /** + * The path for storing and retrieving the previous question ID. This represents + * the QnA question ID from the previous turn. It is stored within the current + * step's {@link WaterfallStepContext}. It supports QnA Maker's follow-up prompt + * and active learning features. + */ + protected static final String PREVIOUS_QNA_ID = "prevQnAId"; + + /** + * The path for storing and retrieving the options for this instance of the + * dialog. This includes the options with which the dialog was started and + * options expected by the QnA Maker service. It is stored within the current + * step's {@link WaterfallStepContext}. It supports QnA Maker and the dialog + * system. + */ + protected static final String OPTIONS = "options"; + + // Dialog Options parameters + + /** + * The default threshold for answers returned, based on score. + */ + protected static final Float DEFAULT_THRESHOLD = 0.3F; + + /** + * The default maximum number of answers to be returned for the question. + */ + protected static final Integer DEFAULT_TOP_N = 3; + + private static final String DEFAULT_NO_ANSWER = "No QnAMaker answers found."; + + // Card parameters + private static final String DEFAULT_CARD_TITLE = "Did you mean:"; + private static final String DEFAULT_CARD_NO_MATCH_TEXT = "None of the above."; + private static final String DEFAULT_CARD_NO_MATCH_RESPONSE = "Thanks for the feedback."; + private static final Integer PERCENTAGE_DIVISOR = 100; + + /** + * Gets the OkHttpClient instance to use for requests to the QnA Maker service. + * + * @return The HTTP client. + */ + public OkHttpClient getHttpClient() { + return this.httpClient; + } + + /** + * Gets the OkHttpClient instance to use for requests to the QnA Maker service. + * + * @param withHttpClient The HTTP client. + */ + public void setHttpClient(OkHttpClient withHttpClient) { + this.httpClient = withHttpClient; + } + + /** + * Gets the QnA Maker knowledge base ID to query. + * + * @return The knowledge base ID or an expression which evaluates to the + * knowledge base ID. + */ + public String getKnowledgeBaseId() { + return this.knowledgeBaseId; + } + + /** + * Sets the QnA Maker knowledge base ID to query. + * + * @param withKnowledgeBaseId The knowledge base ID or an expression which + * evaluates to the knowledge base ID. + */ + public void setKnowledgeBaseId(String withKnowledgeBaseId) { + this.knowledgeBaseId = withKnowledgeBaseId; + } + + /** + * Gets the QnA Maker host URL for the knowledge base. + * + * @return The QnA Maker host URL or an expression which evaluates to the host + * URL. + */ + public String getHostName() { + return this.hostName; + } + + /** + * Sets the QnA Maker host URL for the knowledge base. + * + * @param withHostName The QnA Maker host URL or an expression which evaluates + * to the host URL. + */ + public void setHostName(String withHostName) { + this.hostName = withHostName; + } + + /** + * Gets the QnA Maker endpoint key to use to query the knowledge base. + * + * @return The QnA Maker endpoint key to use or an expression which evaluates to + * the endpoint key. + */ + public String getEndpointKey() { + return this.endpointKey; + } + + /** + * Sets the QnA Maker endpoint key to use to query the knowledge base. + * + * @param withEndpointKey The QnA Maker endpoint key to use or an expression + * which evaluates to the endpoint key. + */ + public void setEndpointKey(String withEndpointKey) { + this.endpointKey = withEndpointKey; + } + + /** + * Gets the threshold for answers returned, based on score. + * + * @return The threshold for answers returned or an expression which evaluates + * to the threshold. + */ + public Float getThreshold() { + return this.threshold; + } + + /** + * Sets the threshold for answers returned, based on score. + * + * @param withThreshold The threshold for answers returned or an expression + * which evaluates to the threshold. + */ + public void setThreshold(Float withThreshold) { + this.threshold = withThreshold; + } + + /** + * Gets the maximum number of answers to return from the knowledge base. + * + * @return The maximum number of answers to return from the knowledge base or an + * expression which evaluates to the maximum number to return. + */ + public Integer getTop() { + return this.top; + } + + /** + * Sets the maximum number of answers to return from the knowledge base. + * + * @param withTop The maximum number of answers to return from the knowledge + * base or an expression which evaluates to the maximum number to + * return. + */ + public void setTop(Integer withTop) { + this.top = withTop; + } + + /** + * Gets the template to send the user when QnA Maker does not find an answer. + * + * @return The template to send the user when QnA Maker does not find an answer. + */ + public BindToActivity getNoAnswer() { + return this.noAnswer; + } + + /** + * Sets the template to send the user when QnA Maker does not find an answer. + * + * @param withNoAnswer The template to send the user when QnA Maker does not + * find an answer. + */ + public void setNoAnswer(BindToActivity withNoAnswer) { + this.noAnswer = withNoAnswer; + } + + /** + * Gets the card title to use when showing active learning options to the user, + * if active learning is enabled. + * + * @return The path card title to use when showing active learning options to + * the user or an expression which evaluates to the card title. + */ + public String getActiveLearningCardTitle() { + return this.activeLearningCardTitle; + } + + /** + * Sets the card title to use when showing active learning options to the user, + * if active learning is enabled. + * + * @param withActiveLearningCardTitle The path card title to use when showing + * active learning options to the user or an + * expression which evaluates to the card + * title. + */ + public void setActiveLearningCardTitle(String withActiveLearningCardTitle) { + this.activeLearningCardTitle = withActiveLearningCardTitle; + } + + /** + * Gets the button text to use with active learning options, allowing a user to + * indicate none of the options are applicable. + * + * @return The button text to use with active learning options or an expression + * which evaluates to the button text. + */ + public String getCardNoMatchText() { + return this.cardNoMatchText; + } + + /** + * Sets the button text to use with active learning options, allowing a user to + * indicate none of the options are applicable. + * + * @param withCardNoMatchText The button text to use with active learning + * options or an expression which evaluates to the + * button text. + */ + public void setCardNoMatchText(String withCardNoMatchText) { + this.cardNoMatchText = withCardNoMatchText; + } + + /** + * Gets the template to send the user if they select the no match option on an + * active learning card. + * + * @return The template to send the user if they select the no match option on + * an active learning card. + */ + public BindToActivity getCardNoMatchResponse() { + return this.cardNoMatchResponse; + } + + /** + * Sets the template to send the user if they select the no match option on an + * active learning card. + * + * @param withCardNoMatchResponse The template to send the user if they select + * the no match option on an active learning + * card. + */ + public void setCardNoMatchResponse(BindToActivity withCardNoMatchResponse) { + this.cardNoMatchResponse = withCardNoMatchResponse; + } + + /** + * Gets the QnA Maker metadata with which to filter or boost queries to the + * knowledge base; or null to apply none. + * + * @return The QnA Maker metadata with which to filter or boost queries to the + * knowledge base or an expression which evaluates to the QnA Maker + * metadata. + */ + public Metadata[] getStrictFilters() { + return this.strictFilters; + } + + /** + * Sets the QnA Maker metadata with which to filter or boost queries to the + * knowledge base; or null to apply none. + * + * @param withStrictFilters The QnA Maker metadata with which to filter or boost + * queries to the knowledge base or an expression which + * evaluates to the QnA Maker metadata. + */ + public void setStrictFilters(Metadata[] withStrictFilters) { + this.strictFilters = withStrictFilters; + } + + /** + * Gets the flag to determine if personal information should be logged in + * telemetry. + * + * @return The flag to indicate in personal information should be logged in + * telemetry. + */ + public Boolean getLogPersonalInformation() { + return this.logPersonalInformation; + } + + /** + * Sets the flag to determine if personal information should be logged in + * telemetry. + * + * @param withLogPersonalInformation The flag to indicate in personal + * information should be logged in telemetry. + */ + public void setLogPersonalInformation(Boolean withLogPersonalInformation) { + this.logPersonalInformation = withLogPersonalInformation; + } + + /** + * Gets a value indicating whether gets or sets environment of knowledgebase to + * be called. + * + * @return A value indicating whether to call test or prod environment of + * knowledge base. + */ + public Boolean getIsTest() { + return this.isTest; + } + + /** + * Sets a value indicating whether gets or sets environment of knowledgebase to + * be called. + * + * @param withIsTest A value indicating whether to call test or prod environment + * of knowledge base. + */ + public void setIsTest(Boolean withIsTest) { + this.isTest = withIsTest; + } + + /** + * Gets the QnA Maker ranker type to use. + * + * @return The QnA Maker ranker type to use or an expression which evaluates to + * the ranker type. + */ + public String getRankerType() { + return this.rankerType; + } + + /** + * Sets the QnA Maker ranker type to use. + * + * @param withRankerType The QnA Maker ranker type to use or an expression which + * evaluates to the ranker type. + */ + public void setRankerType(String withRankerType) { + this.rankerType = withRankerType; + } + + /** + * Initializes a new instance of the @{link QnAMakerDialog} class. + */ + public QnAMakerDialog() { + super("QnAMakerDialog", null); + + // add waterfall steps + this.addStep(this::callGenerateAnswer); + this.addStep(this::callTrain); + this.addStep(this::checkForMultiTurnPrompt); + this.addStep(this::displayQnAResult); + } + + /** + * Initializes a new instance of the @{link QnAMakerDialog} class. + * + * @param dialogId The ID of the @{link Dialog}. + * @param withKnowledgeBaseId The ID of the QnA Maker knowledge base to + * query. + * @param withEndpointKey The QnA Maker endpoint key to use to query + * the knowledge base. + * @param withHostName The QnA Maker host URL for the knowledge + * base, starting with "https://" and ending + * with "/qnamaker". + * @param withNoAnswer The activity to send the user when QnA + * Maker does not find an answer. + * @param withThreshold The threshold for answers returned, based + * on score. + * @param withActiveLearningCardTitle The card title to use when showing active + * learning options to the user, if active + * learning is enabled. + * @param withCardNoMatchText The button text to use with active + * learning options, allowing a user to + * indicate none of the options are + * applicable. + * @param withTop The maximum number of answers to return + * from the knowledge base. + * @param withCardNoMatchResponse The activity to send the user if they + * select the no match option on an active + * learning card. + * @param withStrictFilters QnA Maker metadata with which to filter or + * boost queries to the knowledge base; or + * null to apply none. + * @param withHttpClient An HTTP client to use for requests to the + * QnA Maker Service; or `null` to use a + * default client. + */ + @SuppressWarnings("checkstyle:ParameterNumber") + public QnAMakerDialog( + String dialogId, + String withKnowledgeBaseId, + String withEndpointKey, + String withHostName, + @Nullable Activity withNoAnswer, + Float withThreshold, + String withActiveLearningCardTitle, + String withCardNoMatchText, + Integer withTop, + @Nullable Activity withCardNoMatchResponse, + @Nullable Metadata[] withStrictFilters, + @Nullable OkHttpClient withHttpClient + ) { + super(dialogId, null); + if (knowledgeBaseId == null) { + throw new IllegalArgumentException("knowledgeBaseId"); + } + this.knowledgeBaseId = withKnowledgeBaseId; + if (hostName == null) { + throw new IllegalArgumentException("hostName"); + } + this.hostName = withHostName; + if (withEndpointKey == null) { + throw new IllegalArgumentException("endpointKey"); + } + this.endpointKey = withEndpointKey; + this.threshold = withThreshold != null ? withThreshold : DEFAULT_THRESHOLD; + this.top = withTop != null ? withTop : DEFAULT_TOP_N; + this.activeLearningCardTitle = + withActiveLearningCardTitle != null ? withActiveLearningCardTitle : DEFAULT_CARD_TITLE; + this.cardNoMatchText = withCardNoMatchText != null ? withCardNoMatchText : DEFAULT_CARD_NO_MATCH_TEXT; + this.strictFilters = withStrictFilters; + this.noAnswer = + new BindToActivity(withNoAnswer != null ? withNoAnswer : MessageFactory.text(DEFAULT_NO_ANSWER)); + this.cardNoMatchResponse = new BindToActivity( + withCardNoMatchResponse != null + ? withCardNoMatchResponse + : MessageFactory.text(DEFAULT_CARD_NO_MATCH_RESPONSE) + ); + this.httpClient = withHttpClient; + + // add waterfall steps + this.addStep(this::callGenerateAnswer); + this.addStep(this::callTrain); + this.addStep(this::checkForMultiTurnPrompt); + this.addStep(this::displayQnAResult); + } + + /** + * Initializes a new instance of the {@link QnAMakerDialog} class. + * + * @param withKnowledgeBaseId The ID of the QnA Maker knowledge base to + * query. + * @param withEndpointKey The QnA Maker endpoint key to use to query + * the knowledge base. + * @param withHostName The QnA Maker host URL for the knowledge + * base, starting with "https://" and ending + * with "/qnamaker". + * @param withNoAnswer The activity to send the user when QnA + * Maker does not find an answer. + * @param withThreshold The threshold for answers returned, based + * on score. + * @param withActiveLearningCardTitle The card title to use when showing active + * learning options to the user, if active + * learning is enabled. + * @param withCardNoMatchText The button text to use with active + * learning options, allowing a user to + * indicate none of the options are + * applicable. + * @param withTop The maximum number of answers to return + * from the knowledge base. + * @param withCardNoMatchResponse The activity to send the user if they + * select the no match option on an active + * learning card. + * @param withStrictFilters QnA Maker metadata with which to filter or + * boost queries to the knowledge base; or + * null to apply none. + * @param withHttpClient An HTTP client to use for requests to the + * QnA Maker Service; or `null` to use a + * default client. + */ + @SuppressWarnings("checkstyle:ParameterNumber") + public QnAMakerDialog( + String withKnowledgeBaseId, + String withEndpointKey, + String withHostName, + @Nullable Activity withNoAnswer, + Float withThreshold, + String withActiveLearningCardTitle, + String withCardNoMatchText, + Integer withTop, + @Nullable Activity withCardNoMatchResponse, + @Nullable Metadata[] withStrictFilters, + @Nullable OkHttpClient withHttpClient + ) { + this( + "QnAMakerDialog", + withKnowledgeBaseId, + withEndpointKey, + withHostName, + withNoAnswer, + withThreshold, + withActiveLearningCardTitle, + withCardNoMatchText, + withTop, + withCardNoMatchResponse, + withStrictFilters, + withHttpClient + ); + } + + /** + * Called when the dialog is started and pushed onto the dialog stack. + * + * @param dc The @{link DialogContext} for the current turn of + * conversation. + * @param options Optional, initial information to pass to the dialog. + * @return A Task representing the asynchronous operation. If the task is + * successful, the result indicates whether the dialog is still active + * after the turn has been processed by the dialog. + * + * You can use the @{link options} parameter to include the QnA Maker + * context data, which represents context from the previous query. To do + * so, the value should include a `context` property of type @{link + * QnaResponseContext}. + */ + @Override + public CompletableFuture beginDialog(DialogContext dc, @Nullable Object options) { + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException("dc")); + } + + if (!dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + return CompletableFuture.completedFuture(END_OF_TURN); + } + + QnAMakerOptions qnAMakerOptions = this.getQnAMakerOptions(dc).join(); + QnADialogResponseOptions qnADialogResponseOptions = this.getQnAResponseOptions(dc).join(); + + QnAMakerDialogOptions dialogOptions = new QnAMakerDialogOptions(); + dialogOptions.setQnAMakerOptions(qnAMakerOptions); + dialogOptions.setResponseOptions(qnADialogResponseOptions); + + if (options != null) { + dialogOptions = ObjectPath.assign(dialogOptions, options); + } + + ObjectPath.setPathValue(dc.getActiveDialog().getState(), OPTIONS, dialogOptions); + + return super.beginDialog(dc, dialogOptions); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueDialog(DialogContext dc) { + Boolean interrupted = dc.getState().getValue(TurnPath.INTERRUPTED, false, Boolean.class); + if (interrupted) { + // if qnamaker was interrupted then end the qnamaker dialog + return dc.endDialog(); + } + + return super.continueDialog(dc); + } + + /** + * {@inheritDoc} + */ + @Override + protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) { + if (dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + // decide whether we want to allow interruption or not. + // if we don't get a response from QnA which signifies we expected it, + // then we allow interruption. + + String reply = dc.getContext().getActivity().getText(); + QnAMakerDialogOptions dialogOptions = (QnAMakerDialogOptions) ObjectPath + .getPathValue(dc.getActiveDialog().getState(), OPTIONS, QnAMakerDialogOptions.class); + + if (reply.equalsIgnoreCase(dialogOptions.getResponseOptions().getCardNoMatchText())) { + // it matches nomatch text, we like that. + return CompletableFuture.completedFuture(true); + } + + List suggestedQuestions = (List) dc.getState().get("this.suggestedQuestions"); + if ( + suggestedQuestions != null + && suggestedQuestions.stream().anyMatch(question -> question.compareToIgnoreCase(reply.trim()) == 0) + ) { + // it matches one of the suggested actions, we like that. + return CompletableFuture.completedFuture(true); + } + + // Calling QnAMaker to get response. + return this.getQnAMakerClient(dc).thenCompose(qnaClient -> { + QnAMakerDialog.resetOptions(dc, dialogOptions); + + return qnaClient.getAnswersRaw(dc.getContext(), dialogOptions.getQnAMakerOptions(), null, null) + .thenApply( + response -> { + // cache result so step doesn't have to do it again, this is a turn cache and we + // use hashcode so we don't conflict with any other qnamakerdialogs out there. + dc.getState().setValue(String.format("turn.qnaresult%s", this.hashCode()), response); + + // disable interruption if we have answers. + return !(response.getAnswers().length == 0); + } + ); + }); + } + // call base for default behavior. + return this.onPostBubbleEvent(dc, e); + } + + /** + * Gets an {@link QnAMakerClient} to use to access the QnA Maker knowledge base. + * + * @param dc The {@link DialogContext} for the current turn of conversation. + * @return A Task representing the asynchronous operation. If the task is + * successful, the result contains the QnA Maker client to use. + */ + protected CompletableFuture getQnAMakerClient(DialogContext dc) { + QnAMakerClient qnaClient = (QnAMakerClient) dc.getContext().getTurnState(); + if (qnaClient != null) { + // return mock client + return CompletableFuture.completedFuture(qnaClient); + } + + QnAMakerEndpoint endpoint = new QnAMakerEndpoint(); + endpoint.setEndpointKey(endpointKey); + endpoint.setHost(hostName); + endpoint.setKnowledgeBaseId(knowledgeBaseId); + + return this.getQnAMakerOptions( + dc + ).thenApply(options -> new QnAMaker(endpoint, options, this.getTelemetryClient(), this.logPersonalInformation)); + } + + /** + * Gets the options for the QnA Maker client that the dialog will use to query + * the knowledge base. + * + * @param dc The {@link "DialogContext"} for the current turn of + * conversation. + * @return A {@link "Task"} representing the asynchronous operation. If the + * task is successful, the result contains the QnA Maker options to use. + */ + protected CompletableFuture getQnAMakerOptions(DialogContext dc) { + QnAMakerOptions options = new QnAMakerOptions(); + options.setScoreThreshold(threshold); + options.setStrictFilters(strictFilters); + options.setTop(top); + options.setContext(new QnARequestContext()); + options.setQnAId(0); + options.setRankerType(rankerType); + options.setIsTest(isTest); + return CompletableFuture.completedFuture(options); + } + + /** + * Gets the options the dialog will use to display query results to the user. + * + * @param dc The {@link DialogContext} for the current turn of conversation. + * @return A Task representing the asynchronous operation. If the task is + * successful, the result contains the response options to use. + */ + protected CompletableFuture getQnAResponseOptions(DialogContext dc) { + QnADialogResponseOptions options = new QnADialogResponseOptions(); + options.setNoAnswer(noAnswer.bind(dc, dc.getState()).join()); + options.setActiveLearningCardTitle( + activeLearningCardTitle != null ? activeLearningCardTitle : DEFAULT_CARD_TITLE + ); + options.setCardNoMatchText(cardNoMatchText != null ? cardNoMatchText : DEFAULT_CARD_NO_MATCH_TEXT); + options.setCardNoMatchResponse(cardNoMatchResponse.bind(dc, null).join()); + + return CompletableFuture.completedFuture(options); + } + + /** + * Displays QnA Result from stepContext through Activity - with first answer + * from QnA Maker response. + * + * @param stepContext stepContext. + * @return An object of Task of type {@link DialogTurnResult}. + */ + protected CompletableFuture displayQnAResult(WaterfallStepContext stepContext) { + QnAMakerDialogOptions dialogOptions = + ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), OPTIONS, QnAMakerDialogOptions.class); + String reply = stepContext.getContext().getActivity().getText(); + if (reply.compareToIgnoreCase(dialogOptions.getResponseOptions().getCardNoMatchText()) == 0) { + Activity activity = dialogOptions.getResponseOptions().getCardNoMatchResponse(); + if (activity == null) { + stepContext.getContext().sendActivity(DEFAULT_CARD_NO_MATCH_RESPONSE).join(); + } else { + stepContext.getContext().sendActivity(activity).join(); + } + + return stepContext.endDialog(); + } + + // If previous QnAId is present, replace the dialog + Integer previousQnAId = + ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), PREVIOUS_QNA_ID, Integer.class, 0); + if (previousQnAId > 0) { + // restart the waterfall to step 0 + return this.runStep(stepContext, 0, DialogReason.BEGIN_CALLED, null); + } + + // If response is present then show that response, else default answer. + List response = (List) stepContext.getResult(); + if (response != null && response.size() > 0) { + stepContext.getContext().sendActivity(response.get(0).getAnswer()).join(); + } else { + Activity activity = dialogOptions.getResponseOptions().getNoAnswer(); + if (activity == null) { + stepContext.getContext().sendActivity(DEFAULT_NO_ANSWER).join(); + } else { + stepContext.getContext().sendActivity(activity).join(); + } + } + + return stepContext.endDialog(); + } + + private static void resetOptions(DialogContext dc, QnAMakerDialogOptions dialogOptions) { + // Resetting context and QnAId + dialogOptions.getQnAMakerOptions().setQnAId(0); + dialogOptions.getQnAMakerOptions().setContext(new QnARequestContext()); + + // -Check if previous context is present, if yes then put it with the query + // -Check for id if query is present in reverse index. + Map previousContextData = + ObjectPath.getPathValue( + dc.getActiveDialog().getState(), + QNA_CONTEXT_DATA, + Map.class, + new HashMap()); + Integer previousQnAId = + ObjectPath.getPathValue(dc.getActiveDialog().getState(), PREVIOUS_QNA_ID, Integer.class, 0); + + if (previousQnAId > 0) { + QnARequestContext context = new QnARequestContext(); + context.setPreviousQnAId(previousQnAId); + dialogOptions.getQnAMakerOptions().setContext(context); + + Integer currentQnAId = previousContextData.get(dc.getContext().getActivity().getText()); + if (currentQnAId != null) { + dialogOptions.getQnAMakerOptions().setQnAId(currentQnAId); + } + } + } + + private CompletableFuture callGenerateAnswer(WaterfallStepContext stepContext) { + // clear suggestedQuestions between turns. + stepContext.getState().removeValue("this.suggestedQuestions"); + + QnAMakerDialogOptions dialogOptions = + ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), OPTIONS, QnAMakerDialogOptions.class); + QnAMakerDialog.resetOptions(stepContext, dialogOptions); + + // Storing the context info + stepContext.getValues().put(ValueProperty.CURRENT_QUERY, stepContext.getContext().getActivity().getText()); + + // Calling QnAMaker to get response. + return this.getQnAMakerClient(stepContext).thenApply(qnaMakerClient -> { + QueryResults response = qnaMakerClient + .getAnswersRaw(stepContext.getContext(), dialogOptions.getQnAMakerOptions(), null, null) + .join(); + + // Resetting previous query. + Integer previousQnAId = -1; + ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), PREVIOUS_QNA_ID, previousQnAId); + + // Take this value from GetAnswerResponse + Boolean isActiveLearningEnabled = response.getActiveLearningEnabled(); + + stepContext.getValues().put(ValueProperty.QNA_DATA, Arrays.asList(response.getAnswers())); + + // Check if active learning is enabled. + // MaximumScoreForLowScoreVariation is the score above which no need to check + // for feedback. + if ( + response.getAnswers().length > 0 && response.getAnswers()[0] + .getScore() <= (ActiveLearningUtils.getMaximumScoreForLowScoreVariation() / PERCENTAGE_DIVISOR) + ) { + // Get filtered list of the response that support low score variation criteria. + response.setAnswers(qnaMakerClient.getLowScoreVariation(response.getAnswers())); + + if (response.getAnswers().length > 1 && isActiveLearningEnabled) { + List suggestedQuestions = new ArrayList(); + for (QueryResult qna : response.getAnswers()) { + suggestedQuestions.add(qna.getQuestions()[0]); + } + + // Get active learning suggestion card activity. + Activity message = QnACardBuilder.getSuggestionsCard( + suggestedQuestions, + dialogOptions.getResponseOptions().getActiveLearningCardTitle(), + dialogOptions.getResponseOptions().getCardNoMatchText() + ); + stepContext.getContext().sendActivity(message).join(); + + ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), OPTIONS, dialogOptions); + stepContext.getState().setValue("this.suggestedQuestions", suggestedQuestions); + return new DialogTurnResult(DialogTurnStatus.WAITING); + } + } + + List result = new ArrayList(); + if (!(response.getAnswers().length == 0)) { + result.add(response.getAnswers()[0]); + } + + stepContext.getValues().put(ValueProperty.QNA_DATA, result); + ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), OPTIONS, dialogOptions); + + // If card is not shown, move to next step with top QnA response. + return stepContext.next(result).join(); + }); + } + + private CompletableFuture callTrain(WaterfallStepContext stepContext) { + QnAMakerDialogOptions dialogOptions = + ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), OPTIONS, QnAMakerDialogOptions.class); + List trainResponses = (List) stepContext.getValues().get(ValueProperty.QNA_DATA); + String currentQuery = (String) stepContext.getValues().get(ValueProperty.CURRENT_QUERY); + + String reply = stepContext.getContext().getActivity().getText(); + + if (trainResponses.size() > 1) { + QueryResult qnaResult = + trainResponses.stream().filter(kvp -> kvp.getQuestions()[0].equals(reply)).findFirst().orElse(null); + if (qnaResult != null) { + List queryResultArr = new ArrayList(); + stepContext.getValues().put(ValueProperty.QNA_DATA, queryResultArr.add(qnaResult)); + FeedbackRecord record = new FeedbackRecord(); + record.setUserId(stepContext.getContext().getActivity().getId()); + record.setUserQuestion(currentQuery); + record.setQnaId(qnaResult.getId()); + FeedbackRecord[] records = {record}; + FeedbackRecords feedbackRecords = new FeedbackRecords(); + feedbackRecords.setRecords(records); + // Call Active Learning Train API + return this.getQnAMakerClient(stepContext).thenCompose(qnaClient -> { + try { + return qnaClient.callTrain(feedbackRecords) + .thenCompose(task -> stepContext.next(new ArrayList().add(qnaResult))); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerDialog.class).error("callTrain"); + } + return CompletableFuture.completedFuture(null); + }); + } else if (reply.compareToIgnoreCase(dialogOptions.getResponseOptions().getCardNoMatchText()) == 0) { + Activity activity = dialogOptions.getResponseOptions().getCardNoMatchResponse(); + if (activity == null) { + stepContext.getContext().sendActivity(DEFAULT_CARD_NO_MATCH_RESPONSE).join(); + } else { + stepContext.getContext().sendActivity(activity).join(); + } + + return stepContext.endDialog(); + } else { + // restart the waterfall to step 0 + return runStep(stepContext, 0, DialogReason.BEGIN_CALLED, null); + } + } + + return stepContext.next(stepContext.getResult()); + } + + private CompletableFuture checkForMultiTurnPrompt(WaterfallStepContext stepContext) { + QnAMakerDialogOptions dialogOptions = + ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), OPTIONS, QnAMakerDialogOptions.class); + List response = (List) stepContext.getResult(); + if (response != null && response.size() > 0) { + // -Check if context is present and prompt exists + // -If yes: Add reverse index of prompt display name and its corresponding QnA + // ID + // -Set PreviousQnAId as answer.Id + // -Display card for the prompt + // -Wait for the reply + // -If no: Skip to next step + + QueryResult answer = response.get(0); + + if (answer.getContext() != null && answer.getContext().getPrompts().length > 0) { + Map previousContextData = + ObjectPath.getPathValue( + stepContext.getActiveDialog().getState(), + QNA_CONTEXT_DATA, + Map.class, + new HashMap<>()); + for (QnAMakerPrompt prompt : answer.getContext().getPrompts()) { + previousContextData.put(prompt.getDisplayText(), prompt.getQnaId()); + } + + ObjectPath + .setPathValue(stepContext.getActiveDialog().getState(), QNA_CONTEXT_DATA, previousContextData); + ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), PREVIOUS_QNA_ID, answer.getId()); + ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), OPTIONS, dialogOptions); + + // Get multi-turn prompts card activity. + Activity message = + QnACardBuilder.getQnAPromptsCard(answer, dialogOptions.getResponseOptions().getCardNoMatchText()); + stepContext.getContext().sendActivity(message).join(); + + return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.WAITING)); + } + } + + return stepContext.next(stepContext.getResult()); + } + + /** + * Helper class. + */ + final class ValueProperty { + public static final String CURRENT_QUERY = "currentQuery"; + public static final String QNA_DATA = "qnaData"; + + private ValueProperty() { + } + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialogOptions.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialogOptions.java new file mode 100644 index 000000000..75122e83d --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialogOptions.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.dialogs; + +import com.microsoft.bot.ai.qna.QnADialogResponseOptions; +import com.microsoft.bot.ai.qna.QnAMakerOptions; + +/** + * Defines Dialog Options for QnAMakerDialog. + */ +public class QnAMakerDialogOptions { + private QnAMakerOptions qnaMakerOptions; + + private QnADialogResponseOptions responseOptions; + + /** + * Gets the options for the QnAMaker service. + * + * @return The options for the QnAMaker service. + */ + public QnAMakerOptions getQnAMakerOptions() { + return this.qnaMakerOptions; + } + + /** + * Sets the options for the QnAMaker service. + * + * @param withQnAMakerOptions The options for the QnAMaker service. + */ + public void setQnAMakerOptions(QnAMakerOptions withQnAMakerOptions) { + this.qnaMakerOptions = withQnAMakerOptions; + } + + /** + * Gets the response options for the QnAMakerDialog. + * + * @return The response options for the QnAMakerDialog. + */ + public QnADialogResponseOptions getResponseOptions() { + return this.responseOptions; + } + + /** + * Sets the response options for the QnAMakerDialog. + * + * @param withResponseOptions The response options for the QnAMakerDialog. + */ + public void setResponseOptions(QnADialogResponseOptions withResponseOptions) { + this.responseOptions = withResponseOptions; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerPrompt.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerPrompt.java new file mode 100644 index 000000000..eeb18295b --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerPrompt.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.dialogs; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Prompt Object. + */ +public class QnAMakerPrompt { + private static final Integer DEFAULT_DISPLAY_ORDER = 0; + + @JsonProperty("displayOrder") + private Integer displayOrder = QnAMakerPrompt.DEFAULT_DISPLAY_ORDER; + + @JsonProperty("qnaId") + private Integer qnaId; + + @JsonProperty("displayText") + private String displayText = new String(); + + @JsonProperty("qna") + private Object qna; + + /** + * Gets displayOrder - index of the prompt - used in ordering of the prompts. + * + * @return Display order. + */ + public Integer getDisplayOrder() { + return this.displayOrder; + } + + /** + * Sets displayOrder - index of the prompt - used in ordering of the prompts. + * + * @param withDisplayOrder Display order. + */ + public void setDisplayOrder(Integer withDisplayOrder) { + this.displayOrder = withDisplayOrder; + } + + /** + * Gets qna id corresponding to the prompt - if QnaId is present, QnADTO object + * is ignored. + * + * @return QnA Id. + */ + public Integer getQnaId() { + return this.qnaId; + } + + /** + * Sets qna id corresponding to the prompt - if QnaId is present, QnADTO object + * is ignored. + * + * @param withQnaId QnA Id. + */ + public void setQnaId(Integer withQnaId) { + this.qnaId = withQnaId; + } + + /** + * Gets displayText - Text displayed to represent a follow up question prompt. + * + * @return Display test. + */ + public String getDisplayText() { + return this.displayText; + } + + /** + * Sets displayText - Text displayed to represent a follow up question prompt. + * + * @param withDisplayText Display test. + */ + public void setDisplayText(String withDisplayText) { + this.displayText = withDisplayText; + } + + /** + * Gets the QnADTO returned from the API. + * + * @return The QnA DTO. + */ + public Object getQna() { + return this.qna; + } + + /** + * Sets the QnADTO returned from the API. + * + * @param withQna The QnA DTO. + */ + public void setQna(Object withQna) { + this.qna = withQna; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/package-info.java new file mode 100644 index 000000000..d20c8015b --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.ai.qna.dialogs. + */ +package com.microsoft.bot.ai.qna.dialogs; diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecord.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecord.java new file mode 100644 index 000000000..f96c87729 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecord.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Active learning feedback record. + */ +public class FeedbackRecord { + @JsonProperty("userId") + private String userId; + + @JsonProperty("userQuestion") + private String userQuestion; + + @JsonProperty("qnaId") + private Integer qnaId; + + /** + * Gets the feedback record's user ID. + * + * @return The user ID. + */ + public String getUserId() { + return this.userId; + } + + /** + * Sets the feedback record's user ID. + * + * @param withUserId The user ID. + */ + public void setUserId(String withUserId) { + this.userId = withUserId; + } + + /** + * Gets the question asked by the user. + * + * @return The user question. + */ + public String getUserQuestion() { + return this.userQuestion; + } + + /** + * Sets question asked by the user. + * + * @param withUserQuestion The user question. + */ + public void setUserQuestion(String withUserQuestion) { + this.userQuestion = withUserQuestion; + } + + /** + * Gets the QnA ID. + * + * @return The QnA ID. + */ + public Integer getQnaId() { + return this.qnaId; + } + + /** + * Sets the QnA ID. + * + * @param withQnaId The QnA ID. + */ + public void setQnaId(Integer withQnaId) { + this.qnaId = withQnaId; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecords.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecords.java new file mode 100644 index 000000000..94bc084d4 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecords.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Active learning feedback records. + */ +public class FeedbackRecords { + @JsonProperty("feedbackRecords") + private FeedbackRecord[] records; + + /** + * Gets the list of feedback records. + * + * @return List of {@link FeedbackRecord}. + */ + public FeedbackRecord[] getRecords() { + return this.records; + } + + /** + * Sets the list of feedback records. + * + * @param withRecords List of {@link FeedbackRecord}. + */ + public void setRecords(FeedbackRecord[] withRecords) { + this.records = withRecords; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/Metadata.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/Metadata.java new file mode 100644 index 000000000..62f02b160 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/Metadata.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the Metadata object sent as part of QnA Maker requests. + */ +public class Metadata implements Serializable { + @JsonProperty("name") + private String name; + + @JsonProperty("value") + private String value; + + /** + * Gets the name for the Metadata property. + * + * @return A string. + */ + public String getName() { + return this.name; + } + + /** + * Sets the name for the Metadata property. + * + * @param withName A string. + */ + public void setName(String withName) { + this.name = withName; + } + + /** + * Gets the value for the Metadata property. + * + * @return A string. + */ + public String getValue() { + return this.value; + } + + /** + * Sets the value for the Metadata property. + * + * @param withValue A string. + */ + public void setValue(String withValue) { + this.value = withValue; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAMakerTraceInfo.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAMakerTraceInfo.java new file mode 100644 index 000000000..712cbcd38 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAMakerTraceInfo.java @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.schema.Activity; + +/** + * This class represents all the trace info that we collect from the QnAMaker + * Middleware. + */ +public class QnAMakerTraceInfo { + @JsonProperty("message") + private Activity message; + + @JsonProperty("queryResults") + private QueryResult[] queryResults; + + @JsonProperty("knowledgeBaseId") + private String knowledgeBaseId; + + @JsonProperty("scoreThreshold") + private Float scoreThreshold; + + @JsonProperty("top") + private Integer top; + + @JsonProperty("strictFilters") + private Metadata[] strictFilters; + + @JsonProperty("context") + private QnARequestContext context; + + @JsonProperty("qnaId") + private Integer qnaId; + + @JsonProperty("isTest") + private Boolean isTest; + + @JsonProperty("rankerType") + private String rankerType; + + @Deprecated + @JsonIgnore + private Metadata[] metadataBoost; + + /** + * Gets message which instigated the query to QnAMaker. + * + * @return Message which instigated the query to QnAMaker. + */ + public Activity getMessage() { + return this.message; + } + + /** + * Sets message which instigated the query to QnAMaker. + * + * @param withMessage Message which instigated the query to QnAMaker. + */ + public void setMessage(Activity withMessage) { + this.message = withMessage; + } + + /** + * Gets results that QnAMaker returned. + * + * @return Results that QnAMaker returned. + */ + public QueryResult[] getQueryResults() { + return this.queryResults; + } + + /** + * Sets results that QnAMaker returned. + * + * @param withQueryResult Results that QnAMaker returned. + */ + public void setQueryResults(QueryResult[] withQueryResult) { + this.queryResults = withQueryResult; + } + + /** + * Gets iD of the Knowledgebase that is being used. + * + * @return ID of the Knowledgebase that is being used. + */ + public String getKnowledgeBaseId() { + return this.knowledgeBaseId; + } + + /** + * Sets iD of the Knowledgebase that is being used. + * + * @param withKnowledgeBaseId ID of the Knowledgebase that is being used. + */ + public void setKnowledgeBaseId(String withKnowledgeBaseId) { + this.knowledgeBaseId = withKnowledgeBaseId; + } + + /** + * Gets the minimum score threshold, used to filter returned results. Scores are + * normalized to the range of 0.0 to 1.0 before filtering. + * + * @return The minimum score threshold, used to filter returned results. + */ + public Float getScoreThreshold() { + return this.scoreThreshold; + } + + /** + * Sets the minimum score threshold, used to filter returned results. Scores are + * normalized to the range of 0.0 to 1.0 before filtering + * + * @param withScoreThreshold The minimum score threshold, used to filter + * returned results. + */ + public void setScoreThreshold(Float withScoreThreshold) { + this.scoreThreshold = withScoreThreshold; + } + + /** + * Gets number of ranked results that are asked to be returned. + * + * @return Number of ranked results that are asked to be returned. + */ + public Integer getTop() { + return this.top; + } + + /** + * Sets number of ranked results that are asked to be returned. + * + * @param withTop Number of ranked results that are asked to be returned. + */ + public void setTop(Integer withTop) { + this.top = withTop; + } + + /** + * Gets the filters used to return answers that have the specified metadata. + * + * @return The filters used to return answers that have the specified metadata. + */ + public Metadata[] getStrictFilters() { + return this.strictFilters; + } + + /** + * Sets the filters used to return answers that have the specified metadata. + * + * @param withStrictFilters The filters used to return answers that have the + * specified metadata. + */ + public void setStrictFilters(Metadata[] withStrictFilters) { + this.strictFilters = withStrictFilters; + } + + /** + * Gets context for multi-turn responses. + * + * @return The context from which the QnA was extracted. + */ + public QnARequestContext getContext() { + return this.context; + } + + /** + * Sets context for multi-turn responses. + * + * @param withContext The context from which the QnA was extracted. + */ + public void setContext(QnARequestContext withContext) { + this.context = withContext; + } + + /** + * Gets QnA Id of the current question asked. + * + * @return Id of the current question asked. + */ + public Integer getQnAId() { + return this.qnaId; + } + + /** + * Sets QnA Id of the current question asked. + * + * @param withQnAId Id of the current question asked. + */ + public void setQnAId(Integer withQnAId) { + this.qnaId = withQnAId; + } + + /** + * Gets a value indicating whether gets or sets environment of knowledgebase to + * be called. + * + * @return A value indicating whether to call test or prod environment of + * knowledgebase. + */ + public Boolean getIsTest() { + return this.isTest; + } + + /** + * Sets a value indicating whether gets or sets environment of knowledgebase to + * be called. + * + * @param withIsTest A value indicating whether to call test or prod environment + * of knowledgebase. + */ + public void setIsTest(Boolean withIsTest) { + this.isTest = withIsTest; + } + + /** + * Gets ranker Types. + * + * @return Ranker Types. + */ + public String getRankerType() { + return this.rankerType; + } + + /** + * Sets ranker Types. + * + * @param withRankerType Ranker Types. + */ + public void setRankerType(String withRankerType) { + this.rankerType = withRankerType; + } + + /** + * Gets the {@link Metadata} collection to be sent when calling QnA Maker to + * boost results. + * + * @return An array of {@link Metadata}. + */ + public Metadata[] getMetadataBoost() { + return this.metadataBoost; + } + + /** + * Sets the {@link Metadata} collection to be sent when calling QnA Maker to + * boost results. + * + * @param withMetadataBoost An array of {@link Metadata}. + */ + public void setMetadataBoost(Metadata[] withMetadataBoost) { + this.metadataBoost = withMetadataBoost; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnARequestContext.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnARequestContext.java new file mode 100644 index 000000000..f5134d6ca --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnARequestContext.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The context associated with QnA. Used to mark if the current prompt is + * relevant with a previous question or not. + */ +public class QnARequestContext { + @JsonProperty("previousQnAId") + private Integer previousQnAId; + + @JsonProperty("previousUserQuery") + private String previousUserQuery = new String(); + + /** + * Gets the previous QnA Id that was returned. + * + * @return The previous QnA Id. + */ + public Integer getPreviousQnAId() { + return this.previousQnAId; + } + + /** + * Sets the previous QnA Id that was returned. + * + * @param withPreviousQnAId The previous QnA Id. + */ + public void setPreviousQnAId(Integer withPreviousQnAId) { + this.previousQnAId = withPreviousQnAId; + } + + /** + * Gets the previous user query/question. + * + * @return The previous user query. + */ + public String getPreviousUserQuery() { + return this.previousUserQuery; + } + + /** + * Sets the previous user query/question. + * + * @param withPreviousUserQuery The previous user query. + */ + public void setPreviousUserQuery(String withPreviousUserQuery) { + this.previousUserQuery = withPreviousUserQuery; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAResponseContext.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAResponseContext.java new file mode 100644 index 000000000..53971d832 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAResponseContext.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.ai.qna.dialogs.QnAMakerPrompt; + +/** + * The context associated with QnA. Used to mark if the qna response has related + * prompts to display. + */ +public class QnAResponseContext { + @JsonProperty("prompts") + private QnAMakerPrompt[] prompts; + + /** + * Gets the prompts collection of related prompts. + * + * @return The QnA prompts array. + */ + public QnAMakerPrompt[] getPrompts() { + return this.prompts; + } + + /** + * Sets the prompts collection of related prompts. + * + * @param withPrompts The QnA prompts array. + */ + public void setPrompts(QnAMakerPrompt[] withPrompts) { + this.prompts = withPrompts; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResult.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResult.java new file mode 100644 index 000000000..4090e4741 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResult.java @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an individual result from a knowledge base query. + */ +public class QueryResult { + @JsonProperty("questions") + private String[] questions; + + @JsonProperty("answer") + private String answer; + + @JsonProperty("score") + private Float score; + + @JsonProperty("metadata") + private Metadata[] metadata; + + @JsonProperty("source") + private String source; + + @JsonProperty("id") + private Integer id; + + @JsonProperty("context") + private QnAResponseContext context; + + /** + * Gets the list of questions indexed in the QnA Service for the given answer. + * + * @return The list of questions indexed in the QnA Service for the given + * answer. + */ + public String[] getQuestions() { + return this.questions; + } + + /** + * Sets the list of questions indexed in the QnA Service for the given answer. + * + * @param withQuestions The list of questions indexed in the QnA Service for the + * given answer. + */ + public void setQuestions(String[] withQuestions) { + this.questions = withQuestions; + } + + /** + * Gets the answer text. + * + * @return The answer text. + */ + public String getAnswer() { + return this.answer; + } + + /** + * Sets the answer text. + * + * @param withAnswer The answer text. + */ + public void setAnswer(String withAnswer) { + this.answer = withAnswer; + } + + /** + * Gets the answer's score, from 0.0 (least confidence) to 1.0 (greatest + * confidence). + * + * @return The answer's score, from 0.0 (least confidence) to 1.0 (greatest + * confidence). + */ + public Float getScore() { + return this.score; + } + + /** + * Sets the answer's score, from 0.0 (least confidence) to 1.0 (greatest + * confidence). + * + * @param withScore The answer's score, from 0.0 (least confidence) to 1.0 + * (greatest confidence). + */ + public void setScore(Float withScore) { + this.score = withScore; + } + + /** + * Gets metadata that is associated with the answer. + * + * @return Metadata that is associated with the answer. + */ + public Metadata[] getMetadata() { + return this.metadata; + } + + /** + * Sets metadata that is associated with the answer. + * + * @param withMetadata Metadata that is associated with the answer. + */ + public void setMetadata(Metadata[] withMetadata) { + this.metadata = withMetadata; + } + + /** + * Gets the source from which the QnA was extracted. + * + * @return The source from which the QnA was extracted. + */ + public String getSource() { + return this.source; + } + + /** + * Sets the source from which the QnA was extracted. + * + * @param withSource The source from which the QnA was extracted. + */ + public void setSource(String withSource) { + this.source = withSource; + } + + /** + * Gets the index of the answer in the knowledge base. V3 uses 'qnaId', V4 uses + * 'id'. + * + * @return The index of the answer in the knowledge base. V3 uses 'qnaId', V4 + * uses 'id'. + */ + public Integer getId() { + return this.id; + } + + /** + * Sets the index of the answer in the knowledge base. V3 uses 'qnaId', V4 uses + * 'id'. + * + * @param withId The index of the answer in the knowledge base. V3 uses 'qnaId', + * V4 uses 'id'. + */ + public void setId(Integer withId) { + this.id = withId; + } + + /** + * Gets context for multi-turn responses. + * + * @return The context from which the QnA was extracted. + */ + public QnAResponseContext getContext() { + return this.context; + } + + /** + * Sets context for multi-turn responses. + * + * @param withContext The context from which the QnA was extracted. + */ + public void setContext(QnAResponseContext withContext) { + this.context = withContext; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResults.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResults.java new file mode 100644 index 000000000..84e26c733 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResults.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Contains answers for a user query. + */ +public class QueryResults { + @JsonProperty("answers") + private QueryResult[] answers; + + @JsonProperty("activeLearningEnabled") + private Boolean activeLearningEnabled; + + /** + * Gets the answers for a user query, sorted in decreasing order of ranking + * score. + * + * @return The answers for a user query, sorted in decreasing order of ranking + * score. + */ + public QueryResult[] getAnswers() { + return this.answers; + } + + /** + * Sets the answers for a user query, sorted in decreasing order of ranking + * score. + * + * @param withAnswers The answers for a user query, sorted in decreasing order + * of ranking score. + */ + public void setAnswers(QueryResult[] withAnswers) { + this.answers = withAnswers; + } + + /** + * Gets a value indicating whether gets or set for the active learning enable + * flag. + * + * @return The active learning enable flag. + */ + public Boolean getActiveLearningEnabled() { + return this.activeLearningEnabled; + } + + /** + * Sets a value indicating whether gets or set for the active learning enable + * flag. + * + * @param withActiveLearningEnabled The active learning enable flag. + */ + public void setActiveLearningEnabled(Boolean withActiveLearningEnabled) { + this.activeLearningEnabled = withActiveLearningEnabled; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/RankerTypes.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/RankerTypes.java new file mode 100644 index 000000000..da36cc393 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/RankerTypes.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.models; + +/** + * Enumeration of types of ranking. + */ +public final class RankerTypes { + /** + * Default Ranker Behaviour. i.e. Ranking based on Questions and Answer. + */ + public static final String DEFAULT_RANKER_TYPE = "Default"; + + /** + * Ranker based on question Only. + */ + public static final String QUESTION_ONLY = "QuestionOnly"; + + /** + * Ranker based on Autosuggest for question field Only. + */ + public static final String AUTO_SUGGEST_QUESTION = "AutoSuggestQuestion"; + + private RankerTypes() { + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/package-info.java new file mode 100644 index 000000000..10e6bf1d1 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.ai.qna.models. + */ +package com.microsoft.bot.ai.qna.models; diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/package-info.java new file mode 100644 index 000000000..692c85b66 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.ai.qna. + */ +@Deprecated +package com.microsoft.bot.ai.qna; diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/ActiveLearningUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/ActiveLearningUtils.java new file mode 100644 index 000000000..3275751fc --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/ActiveLearningUtils.java @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +import com.microsoft.bot.ai.qna.models.QueryResult; + +import java.util.ArrayList; +import java.util.List; + +/** + * Active learning helper class. + */ +public final class ActiveLearningUtils { + /** + * Previous Low Score Variation Multiplier.ActiveLearningUtils. + */ + private static final Float PREVIOUS_LOW_SCORE_VARIATION_MULTIPLIER = 0.7f; + + /** + * Max Low Score Variation Multiplier. + */ + private static final Float MAX_LOW_SCORE_VARIATION_MULTIPLIER = 1.0f; + + private static final Integer PERCENTAGE_DIVISOR = 100; + + private static final Float MAXIMUM_SCORE_VARIATION = 95.0F; + + private static final Float MINIMUM_SCORE_VARIATION = 20.0F; + + private static Float maximumScoreForLowScoreVariation = MAXIMUM_SCORE_VARIATION; + + private static Float minimumScoreForLowScoreVariation = MINIMUM_SCORE_VARIATION; + + private ActiveLearningUtils() { + } + + /** + * Gets maximum Score For Low Score Variation. + * + * @return Maximum Score For Low Score Variation. + */ + public static Float getMaximumScoreForLowScoreVariation() { + return ActiveLearningUtils.maximumScoreForLowScoreVariation; + } + + /** + * Sets maximum Score For Low Score Variation. + * + * @param withMaximumScoreForLowScoreVariation Maximum Score For Low Score + * Variation. + */ + public static void setMaximumScoreForLowScoreVariation(Float withMaximumScoreForLowScoreVariation) { + ActiveLearningUtils.maximumScoreForLowScoreVariation = withMaximumScoreForLowScoreVariation; + } + + /** + * Gets minimum Score For Low Score Variation. + * + * @return Minimum Score For Low Score Variation. + */ + public static Float getMinimumScoreForLowScoreVariation() { + return ActiveLearningUtils.minimumScoreForLowScoreVariation; + } + + /** + * Sets minimum Score For Low Score Variation. + * + * @param withMinimumScoreForLowScoreVariation Minimum Score For Low Score + * Variation. + */ + public static void setMinimumScoreForLowScoreVariation(Float withMinimumScoreForLowScoreVariation) { + ActiveLearningUtils.minimumScoreForLowScoreVariation = withMinimumScoreForLowScoreVariation; + } + + /** + * Returns list of qnaSearch results which have low score variation. + * + * @param qnaSearchResults List of QnaSearch results. + * @return List of filtered qnaSearch results. + */ + public static List getLowScoreVariation(List qnaSearchResults) { + List filteredQnaSearchResult = new ArrayList(); + + if (qnaSearchResults == null || qnaSearchResults.isEmpty()) { + return filteredQnaSearchResult; + } + + if (qnaSearchResults.size() == 1) { + return qnaSearchResults; + } + + Float topAnswerScore = qnaSearchResults.get(0).getScore() * PERCENTAGE_DIVISOR; + if (topAnswerScore > ActiveLearningUtils.maximumScoreForLowScoreVariation) { + filteredQnaSearchResult.add(qnaSearchResults.get(0)); + return filteredQnaSearchResult; + } + + Float prevScore = topAnswerScore; + + if (topAnswerScore > ActiveLearningUtils.minimumScoreForLowScoreVariation) { + filteredQnaSearchResult.add(qnaSearchResults.get(0)); + + for (int i = 1; i < qnaSearchResults.size(); i++) { + if ( + ActiveLearningUtils.includeForClustering( + prevScore, + qnaSearchResults.get(i).getScore() * PERCENTAGE_DIVISOR, + ActiveLearningUtils.PREVIOUS_LOW_SCORE_VARIATION_MULTIPLIER + ) && ActiveLearningUtils.includeForClustering( + topAnswerScore, + qnaSearchResults.get(i).getScore() * PERCENTAGE_DIVISOR, + ActiveLearningUtils.MAX_LOW_SCORE_VARIATION_MULTIPLIER + ) + ) { + prevScore = qnaSearchResults.get(i).getScore() * PERCENTAGE_DIVISOR; + filteredQnaSearchResult.add(qnaSearchResults.get(i)); + } + } + } + + return filteredQnaSearchResult; + } + + private static Boolean includeForClustering(Float prevScore, Float currentScore, Float multiplier) { + return (prevScore - currentScore) < (multiplier * Math.sqrt(prevScore)); + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/BindToActivity.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/BindToActivity.java new file mode 100644 index 000000000..d365cbc34 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/BindToActivity.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.schema.Activity; + +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +/** + * Class to bind activities. + */ +public class BindToActivity { + private final Activity activity; + + /** + * Construct to bind an Activity. + * + * @param withActivity activity to bind. + */ + public BindToActivity(Activity withActivity) { + this.activity = withActivity; + } + + /** + * + * @param context The context. + * @param data The data. + * @return The activity. + */ + public CompletableFuture bind(DialogContext context, @Nullable Object data) { + return CompletableFuture.completedFuture(this.activity); + } + + /** + * Get the activity text. + * + * @return The activity text. + */ + public String toString() { + return String.format("%s", this.activity.getText()); + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/GenerateAnswerUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/GenerateAnswerUtils.java new file mode 100644 index 000000000..298718004 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/GenerateAnswerUtils.java @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +import com.microsoft.bot.connector.Async; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.ai.qna.QnAMaker; +import com.microsoft.bot.ai.qna.QnAMakerEndpoint; +import com.microsoft.bot.ai.qna.QnAMakerOptions; +import com.microsoft.bot.ai.qna.models.Metadata; +import com.microsoft.bot.ai.qna.models.QnAMakerTraceInfo; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.ai.qna.models.QueryResults; +import com.microsoft.bot.ai.qna.models.RankerTypes; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; + +import net.minidev.json.JSONObject; +import org.slf4j.LoggerFactory; + +/** + * Helper class for Generate Answer API. + */ +public class GenerateAnswerUtils { + private QnAMakerEndpoint endpoint; + private QnAMakerOptions options; + + private static final Integer PERCENTAGE_DIVISOR = 100; + private static final Float SCORE_THRESHOLD = 0.3f; + private static final Double TIMEOUT = 100000d; + + /** + * Initializes a new instance of the {@link GenerateAnswerUtils} class. + * + * @param withEndpoint QnA Maker endpoint details. + * @param withOptions QnA Maker options. + */ + public GenerateAnswerUtils(QnAMakerEndpoint withEndpoint, QnAMakerOptions withOptions) { + this.endpoint = withEndpoint; + + this.options = withOptions != null ? withOptions : new QnAMakerOptions(); + GenerateAnswerUtils.validateOptions(this.options); + } + + /** + * Gets qnA Maker options. + * + * @return The options for QnAMaker. + */ + public QnAMakerOptions getOptions() { + return this.options; + } + + /** + * Sets qnA Maker options. + * + * @param withOptions The options for QnAMaker. + */ + public void setOptions(QnAMakerOptions withOptions) { + this.options = withOptions; + } + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question to be + * queried against your knowledge base. + * @param messageActivity Message activity of the turn context. + * @param withOptions The options for the QnA Maker knowledge base. If null, + * constructor option is used for this instance. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + * @throws IOException IOException + */ + @Deprecated + public CompletableFuture getAnswers( + TurnContext turnContext, + Activity messageActivity, + QnAMakerOptions withOptions + ) throws IOException { + return this.getAnswersRaw(turnContext, messageActivity, withOptions).thenApply(result -> result.getAnswers()); + } + + /** + * Generates an answer from the knowledge base. + * + * @param turnContext The Turn Context that contains the user question to be + * queried against your knowledge base. + * @param messageActivity Message activity of the turn context. + * @param withOptions The options for the QnA Maker knowledge base. If null, + * constructor option is used for this instance. + * @return A list of answers for the user query, sorted in decreasing order of + * ranking score. + */ + public CompletableFuture getAnswersRaw( + TurnContext turnContext, + Activity messageActivity, + QnAMakerOptions withOptions + ) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException("turnContext")); + } + + if (turnContext.getActivity() == null) { + return Async.completeExceptionally( + new IllegalArgumentException( + String.format( + "The %1$s property for %2$s can't be null: turnContext", + turnContext.getActivity(), + "turnContext" + ) + ) + ); + } + + if (messageActivity == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity type is not a message")); + } + + QnAMakerOptions hydratedOptions = this.hydrateOptions(withOptions); + GenerateAnswerUtils.validateOptions(hydratedOptions); + + try { + return this.queryQnaService(messageActivity, hydratedOptions).thenCompose(result -> { + this.emitTraceInfo(turnContext, messageActivity, result.getAnswers(), hydratedOptions); + return CompletableFuture.completedFuture(result); + }); + } catch (IOException e) { + LoggerFactory.getLogger(GenerateAnswerUtils.class).error("getAnswersRaw"); + return CompletableFuture.completedFuture(null); + } + } + + private static CompletableFuture formatQnAResult( + JsonNode response, + QnAMakerOptions options + ) throws IOException { + String jsonResponse = null; + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + QueryResults results = null; + + jsonResponse = response.toString(); + results = jacksonAdapter.deserialize(jsonResponse, QueryResults.class); + for (QueryResult answer : results.getAnswers()) { + answer.setScore(answer.getScore() / PERCENTAGE_DIVISOR); + } + List answerList = Arrays.asList(results.getAnswers()) + .stream() + .filter(answer -> answer.getScore() > options.getScoreThreshold()) + .collect(Collectors.toList()); + results.setAnswers(answerList.toArray(new QueryResult[answerList.size()])); + + return CompletableFuture.completedFuture(results); + } + + private static void validateOptions(QnAMakerOptions options) { + if (options.getScoreThreshold() == 0) { + options.setScoreThreshold(SCORE_THRESHOLD); + } + + if (options.getTop() == 0) { + options.setTop(1); + } + + if (options.getScoreThreshold() < 0 || options.getScoreThreshold() > 1) { + throw new IllegalArgumentException( + String.format("options: The %s property should be a value between 0 and 1", options.getScoreThreshold()) + ); + } + + if (options.getTimeout() == 0.0d) { + options.setTimeout(TIMEOUT); + } + + if (options.getTop() < 1) { + throw new IllegalArgumentException("options: The top property should be an integer greater than 0"); + } + + if (options.getStrictFilters() == null) { + options.setStrictFilters(new Metadata[0]); + } + + if (options.getRankerType() == null) { + options.setRankerType(RankerTypes.DEFAULT_RANKER_TYPE); + } + } + + /** + * Combines QnAMakerOptions passed into the QnAMaker constructor with the + * options passed as arguments into GetAnswersAsync(). + * + * @param queryOptions The options for the QnA Maker knowledge base. + * @return Return modified options for the QnA Maker knowledge base. + */ + private QnAMakerOptions hydrateOptions(QnAMakerOptions queryOptions) { + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + QnAMakerOptions hydratedOptions = null; + + try { + hydratedOptions = jacksonAdapter.deserialize(jacksonAdapter.serialize(options), QnAMakerOptions.class); + } catch (IOException e) { + LoggerFactory.getLogger(GenerateAnswerUtils.class).error("hydrateOptions"); + } + + if (queryOptions != null) { + if ( + queryOptions.getScoreThreshold() != hydratedOptions.getScoreThreshold() + && queryOptions.getScoreThreshold() != 0 + ) { + hydratedOptions.setScoreThreshold(queryOptions.getScoreThreshold()); + } + + if (queryOptions.getTop() != hydratedOptions.getTop() && queryOptions.getTop() != 0) { + hydratedOptions.setTop(queryOptions.getTop()); + } + + if (queryOptions.getStrictFilters() != null && queryOptions.getStrictFilters().length > 0) { + hydratedOptions.setStrictFilters(queryOptions.getStrictFilters()); + } + + hydratedOptions.setContext(queryOptions.getContext()); + hydratedOptions.setQnAId(queryOptions.getQnAId()); + hydratedOptions.setIsTest(queryOptions.getIsTest()); + hydratedOptions.setRankerType( + queryOptions.getRankerType() != null ? queryOptions.getRankerType() : RankerTypes.DEFAULT_RANKER_TYPE + ); + hydratedOptions.setStrictFiltersJoinOperator(queryOptions.getStrictFiltersJoinOperator()); + } + + return hydratedOptions; + } + + private CompletableFuture queryQnaService( + Activity messageActivity, + QnAMakerOptions withOptions + ) throws IOException { + String requestUrl = String.format( + "%1$s/knowledgebases/%2$s/generateanswer", + this.endpoint.getHost(), + this.endpoint.getKnowledgeBaseId() + ); + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + String jsonRequest = null; + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("question", messageActivity.getText()); + jsonObject.put("top", withOptions.getTop()); + jsonObject.put("strictFilters", withOptions.getStrictFilters()); + jsonObject.put("scoreThreshold", withOptions.getScoreThreshold()); + jsonObject.put("context", withOptions.getContext()); + jsonObject.put("qnaId", withOptions.getQnAId()); + jsonObject.put("isTest", withOptions.getIsTest()); + jsonObject.put("rankerType", withOptions.getRankerType()); + jsonObject.put("StrictFiltersCompoundOperationType", withOptions.getStrictFiltersJoinOperator()); + + jsonRequest = jacksonAdapter.serialize(jsonObject); + + HttpRequestUtils httpRequestHelper = new HttpRequestUtils(); + return httpRequestHelper.executeHttpRequest(requestUrl, jsonRequest, this.endpoint).thenCompose(response -> { + try { + return GenerateAnswerUtils.formatQnAResult(response, withOptions); + } catch (IOException e) { + LoggerFactory.getLogger(GenerateAnswerUtils.class).error("QueryQnAService", e); + return CompletableFuture.completedFuture(null); + } + }); + } + + private CompletableFuture emitTraceInfo( + TurnContext turnContext, + Activity messageActivity, + QueryResult[] result, + QnAMakerOptions withOptions + ) { + String knowledgeBaseId = this.endpoint.getKnowledgeBaseId(); + QnAMakerTraceInfo traceInfo = new QnAMakerTraceInfo(); + traceInfo.setMessage(messageActivity); + traceInfo.setQueryResults(result); + traceInfo.setKnowledgeBaseId(knowledgeBaseId); + traceInfo.setScoreThreshold(withOptions.getScoreThreshold()); + traceInfo.setTop(withOptions.getTop()); + traceInfo.setStrictFilters(withOptions.getStrictFilters()); + traceInfo.setContext(withOptions.getContext()); + traceInfo.setQnAId(withOptions.getQnAId()); + traceInfo.setIsTest(withOptions.getIsTest()); + traceInfo.setRankerType(withOptions.getRankerType()); + + Activity traceActivity = Activity.createTraceActivity( + QnAMaker.QNA_MAKER_NAME, + QnAMaker.QNA_MAKER_TRACE_TYPE, + traceInfo, + QnAMaker.QNA_MAKER_TRACE_LABEL + ); + return turnContext.sendActivity(traceActivity).thenApply(response -> null); + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/HttpRequestUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/HttpRequestUtils.java new file mode 100644 index 000000000..3ffd9e8a9 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/HttpRequestUtils.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +import com.microsoft.bot.connector.Async; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.ai.qna.QnAMakerEndpoint; +import com.microsoft.bot.connector.UserAgent; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.slf4j.LoggerFactory; + +/** + * Helper for HTTP requests. + */ +public class HttpRequestUtils { + private final OkHttpClient httpClient = new OkHttpClient(); + + /** + * Execute Http request. + * + * @param requestUrl Http request url. + * @param payloadBody Http request body. + * @param endpoint QnA Maker endpoint details. + * @return Returns http response object. + */ + public CompletableFuture executeHttpRequest( + String requestUrl, + String payloadBody, + QnAMakerEndpoint endpoint + ) { + if (requestUrl == null) { + return Async + .completeExceptionally(new IllegalArgumentException("requestUrl: Request url can not be null.")); + } + + if (payloadBody == null) { + return Async + .completeExceptionally(new IllegalArgumentException("payloadBody: Payload body can not be null.")); + } + + if (endpoint == null) { + return Async.completeExceptionally(new IllegalArgumentException("endpoint")); + } + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + String endpointKey = endpoint.getEndpointKey(); + Response response; + JsonNode qnaResponse = null; + try { + Request request = buildRequest(requestUrl, endpointKey, buildRequestBody(payloadBody)); + response = this.httpClient.newCall(request).execute(); + qnaResponse = mapper.readTree(response.body().string()); + if (!response.isSuccessful()) { + String message = "Unexpected code " + response.code(); + return Async.completeExceptionally(new Exception(message)); + } + } catch (Exception e) { + LoggerFactory.getLogger(HttpRequestUtils.class).error("findPackages", e); + return Async.completeExceptionally(e); + } + + return CompletableFuture.completedFuture(qnaResponse); + } + + private Request buildRequest(String requestUrl, String endpointKey, RequestBody body) { + HttpUrl.Builder httpBuilder = HttpUrl.parse(requestUrl).newBuilder(); + Request.Builder requestBuilder = new Request.Builder().url(httpBuilder.build()) + .addHeader("Authorization", String.format("EndpointKey %s", endpointKey)) + .addHeader("Ocp-Apim-Subscription-Key", endpointKey) + .addHeader("User-Agent", UserAgent.value()) + .post(body); + return requestBuilder.build(); + } + + private RequestBody buildRequestBody(String payloadBody) throws JsonProcessingException { + return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), payloadBody); + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnACardBuilder.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnACardBuilder.java new file mode 100644 index 000000000..1cc68c09e --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnACardBuilder.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +import java.util.ArrayList; +import java.util.List; + +import com.microsoft.bot.ai.qna.dialogs.QnAMakerPrompt; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.CardAction; +import com.microsoft.bot.schema.HeroCard; + +/** + * Message activity card builder for QnAMaker dialogs. + */ +public final class QnACardBuilder { + + private QnACardBuilder() { + } + + /** + * Get active learning suggestions card. + * + * @param suggestionsList List of suggested questions. + * @param cardTitle Title of the cards + * @param cardNoMatchText No match text. + * @return Activity. + */ + public static Activity getSuggestionsCard(List suggestionsList, String cardTitle, String cardNoMatchText) { + if (suggestionsList == null) { + throw new IllegalArgumentException("suggestionsList"); + } + + if (cardTitle == null) { + throw new IllegalArgumentException("cardTitle"); + } + + if (cardNoMatchText == null) { + throw new IllegalArgumentException("cardNoMatchText"); + } + + Activity chatActivity = Activity.createMessageActivity(); + chatActivity.setText(cardTitle); + List buttonList = new ArrayList(); + + // Add all suggestions + for (String suggestion : suggestionsList) { + CardAction cardAction = new CardAction(); + cardAction.setValue(suggestion); + cardAction.setType(ActionTypes.IM_BACK); + cardAction.setTitle(suggestion); + + buttonList.add(cardAction); + } + + // Add No match text + CardAction cardAction = new CardAction(); + cardAction.setValue(cardNoMatchText); + cardAction.setType(ActionTypes.IM_BACK); + cardAction.setTitle(cardNoMatchText); + buttonList.add(cardAction); + + HeroCard plCard = new HeroCard(); + plCard.setButtons(buttonList); + + // Create the attachment. + Attachment attachment = plCard.toAttachment(); + + chatActivity.setAttachment(attachment); + + return chatActivity; + } + + /** + * Get active learning suggestions card. + * + * @param result Result to be dispalyed as prompts. + * @param cardNoMatchText No match text. + * @return Activity. + */ + public static Activity getQnAPromptsCard(QueryResult result, String cardNoMatchText) { + if (result == null) { + throw new IllegalArgumentException("result"); + } + + if (cardNoMatchText == null) { + throw new IllegalArgumentException("cardNoMatchText"); + } + + Activity chatActivity = Activity.createMessageActivity(); + chatActivity.setText(result.getAnswer()); + List buttonList = new ArrayList(); + + // Add all prompt + for (QnAMakerPrompt prompt : result.getContext().getPrompts()) { + CardAction card = new CardAction(); + card.setValue(prompt.getDisplayText()); + card.setType(ActionTypes.IM_BACK); + card.setTitle(prompt.getDisplayText()); + buttonList.add(card); + } + + HeroCard plCard = new HeroCard(); + plCard.setButtons(buttonList); + + // Create the attachment. + Attachment attachment = plCard.toAttachment(); + + chatActivity.setAttachment(attachment); + + return chatActivity; + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnATelemetryConstants.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnATelemetryConstants.java new file mode 100644 index 000000000..9827f6bd9 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnATelemetryConstants.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +/** + * Default QnA event and property names logged using IBotTelemetryClient. + */ +public final class QnATelemetryConstants { + + private QnATelemetryConstants() { + } + + /** + * The Key used for the custom event type within telemetry. + */ + public static final String QNA_MSG_EVENT = "QnaMessage"; // Event name + + /** + * The Key used when storing a QnA Knowledge Base ID in a custom event within + * telemetry. + */ + public static final String KNOWLEDGE_BASE_ID_PROPERTY = "knowledgeBaseId"; + + /** + * The Key used when storing a QnA Answer in a custom event within telemetry. + */ + public static final String ANSWER_PROPERTY = "answer"; + + /** + * The Key used when storing a flag indicating if a QnA article was found in a + * custom event within telemetry. + */ + public static final String ARTICLE_FOUND_PROPERTY = "articleFound"; + + /** + * The Key used when storing the Channel ID in a custom event within telemetry. + */ + public static final String CHANNEL_ID_PROPERTY = "channelId"; + + /** + * The Key used when storing a matched question ID in a custom event within + * telemetry. + */ + public static final String MATCHED_QUESTION_PROPERTY = "matchedQuestion"; + + /** + * The Key used when storing the identified question text in a custom event + * within telemetry. + */ + public static final String QUESTION_PROPERTY = "question"; + + /** + * The Key used when storing the identified question ID in a custom event within + * telemetry. + */ + public static final String QUESTION_ID_PROPERTY = "questionId"; + + /** + * The Key used when storing a QnA Maker result score in a custom event within + * telemetry. + */ + public static final String SCORE_PROPERTY = "score"; + + /** + * The Key used when storing a username in a custom event within telemetry. + */ + public static final String USERNAME_PROPERTY = "username"; +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/TrainUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/TrainUtils.java new file mode 100644 index 000000000..3bdb9a086 --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/TrainUtils.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna.utils; + +import com.microsoft.bot.ai.qna.QnAMakerEndpoint; +import com.microsoft.bot.ai.qna.models.FeedbackRecords; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +/** + * Helper class for train API. + */ +public class TrainUtils { + private QnAMakerEndpoint endpoint; + + /** + * Initializes a new instance of the {@link TrainUtils} class. + * + * @param withEndpoint QnA Maker endpoint details. + */ + public TrainUtils(QnAMakerEndpoint withEndpoint) { + this.endpoint = withEndpoint; + } + + /** + * Train API to provide feedback. + * + * @param feedbackRecords Feedback record list. + * @return A Task representing the asynchronous operation. + * @throws IOException IOException + */ + public CompletableFuture callTrain(FeedbackRecords feedbackRecords) throws IOException { + if (feedbackRecords == null) { + return Async.completeExceptionally( + new IllegalArgumentException("feedbackRecords: Feedback records cannot be null.") + ); + } + + if (feedbackRecords.getRecords() == null || feedbackRecords.getRecords().length == 0) { + return CompletableFuture.completedFuture(null); + } + + // Call train + return this.queryTrain(feedbackRecords); + } + + private CompletableFuture queryTrain(FeedbackRecords feedbackRecords) throws IOException { + String requestUrl = String + .format("%1$s/knowledgebases/%2$s/train", this.endpoint.getHost(), this.endpoint.getKnowledgeBaseId()); + + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + String jsonRequest = jacksonAdapter.serialize(feedbackRecords); + + HttpRequestUtils httpRequestHelper = new HttpRequestUtils(); + return httpRequestHelper.executeHttpRequest(requestUrl, jsonRequest, this.endpoint).thenApply(result -> null); + } +} diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/package-info.java new file mode 100644 index 000000000..f21e0372a --- /dev/null +++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.ai.qna.utils. + */ +package com.microsoft.bot.ai.qna.utils; diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/MyTurnContext.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/MyTurnContext.java new file mode 100644 index 000000000..872955189 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/MyTurnContext.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.DeleteActivityHandler; +import com.microsoft.bot.builder.SendActivitiesHandler; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextStateCollection; +import com.microsoft.bot.builder.UpdateActivityHandler; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class MyTurnContext implements TurnContext { + + private BotAdapter adapter; + private Activity activity; + + public MyTurnContext(BotAdapter withAdapter, Activity withActivity) { + this.adapter = withAdapter; + this.activity = withActivity; + } + + public String getLocale() { + throw new UnsupportedOperationException(); + } + + public void setLocale(String withLocale) { + throw new UnsupportedOperationException(); + } + + public BotAdapter getAdapter() { + return adapter; + } + + public Activity getActivity() { + return activity; + } + + public TurnContextStateCollection getTurnState() { + throw new UnsupportedOperationException(); + } + + public boolean getResponded() { + throw new UnsupportedOperationException(); + } + + public CompletableFuture deleteActivity(String activityId) { + throw new UnsupportedOperationException(); + } + + public CompletableFuture deleteActivity(ConversationReference conversationReference) { + throw new UnsupportedOperationException(); + } + + public TurnContext onDeleteActivity(DeleteActivityHandler handler) { + throw new UnsupportedOperationException(); + } + + public TurnContext onSendActivities(SendActivitiesHandler handler) { + throw new UnsupportedOperationException(); + } + + public TurnContext onUpdateActivity(UpdateActivityHandler handler) { + throw new UnsupportedOperationException(); + } + + public CompletableFuture sendActivities(List activities) { + throw new UnsupportedOperationException(); + } + + public CompletableFuture sendActivity(String textReplyToSend, String speak, + InputHints inputHint) { + inputHint = inputHint != null ? inputHint : InputHints.ACCEPTING_INPUT; + throw new UnsupportedOperationException(); + } + + public CompletableFuture sendActivity(Activity activity) { + throw new UnsupportedOperationException(); + } + + public CompletableFuture sendActivity(String textToReply) { + throw new UnsupportedOperationException(); + } + + public CompletableFuture sendActivity(String textReplyToSend, String speak) { + throw new UnsupportedOperationException(); + } + + public CompletableFuture updateActivity(Activity activity) { + throw new UnsupportedOperationException(); + } + +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerCardEqualityComparer.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerCardEqualityComparer.java new file mode 100644 index 000000000..81d40abd1 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerCardEqualityComparer.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; + +public class QnAMakerCardEqualityComparer { + + public Boolean Equals(Activity x, Activity y) { + if (x == null && y == null) { + return true; + } + + if(x == null || y == null) { + return false; + } + + if(x.isType(ActivityTypes.MESSAGE) && y.isType(ActivityTypes.MESSAGE)) { + Activity activity1 = x; + Activity activity2 = y; + + if (activity1 == null || activity2 == null) { + return false; + } + + // Check for attachments + if (activity1.getAttachments() != null && activity2.getAttachments() != null) { + if(activity1.getAttachments().size() != activity2.getAttachments().size()) { + return false; + } + } + return true; + } + + return false; + } + + public Integer getHashCode(Activity obj) { + return obj.getId().hashCode(); + } + +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerRecognizerTests.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerRecognizerTests.java new file mode 100644 index 000000000..f8e0526ea --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerRecognizerTests.java @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogSet; +import com.microsoft.bot.dialogs.DialogState; +import com.microsoft.bot.schema.Activity; + +import okhttp3.HttpUrl; +import org.apache.commons.io.FileUtils; +import org.junit.Assert; +import org.junit.Test; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; + +import static org.junit.Assert.fail; + +public class QnAMakerRecognizerTests { + private final String knowledgeBaseId = "dummy-id"; + private final String endpointKey = "dummy-key"; + private final String hostname = "http://localhost"; + private final Boolean mockQnAResponse = true; + + @Test + public void logPiiIsFalseByDefault() { + QnAMakerRecognizer recognizer = new QnAMakerRecognizer(); + recognizer.setHostName(hostname); + recognizer.setEndpointKey(endpointKey); + recognizer.setKnowledgeBaseId(knowledgeBaseId); + + Boolean logPersonalInfo = recognizer.getLogPersonalInformation(); + // Should be false by default, when not specified by user. + Assert.assertFalse(logPersonalInfo); + } + + @Test + public void noTextNoAnswer() { + Activity activity = Activity.createMessageActivity(); + TurnContext context = new TurnContextImpl(new TestAdapter(), activity); + DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState()); + QnAMakerRecognizer recognizer = new QnAMakerRecognizer(); + recognizer.setHostName(hostname); + recognizer.setKnowledgeBaseId(knowledgeBaseId); + recognizer.setEndpointKey(endpointKey); + + RecognizerResult result = recognizer.recognize(dc, activity).join(); + Assert.assertEquals(result.getEntities(), null); + Assert.assertEquals(result.getProperties().get("answers"), null); + Assert.assertEquals(result.getIntents().get("QnAMatch"), null); + Assert.assertNotEquals(result.getIntents().get("None"), null); + } + + @Test + public void noAnswer() { + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsNoAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + // Set mock response in MockWebServer + String url = "/qnamaker/knowledgebases/"; + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerRecognizer recognizer = new QnAMakerRecognizer(); + recognizer.setHostName(finalEndpoint); + recognizer.setKnowledgeBaseId(knowledgeBaseId); + recognizer.setEndpointKey(endpointKey); + + Activity activity = Activity.createMessageActivity(); + activity.setText("test"); + TurnContext context = new TurnContextImpl(new TestAdapter(), activity); + DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState()); + RecognizerResult result = recognizer.recognize(dc, activity).join(); + Assert.assertEquals(result.getEntities(), null); + Assert.assertEquals(result.getProperties().get("answers"), null); + Assert.assertEquals(result.getIntents().get("QnAMatch"), null); + Assert.assertNotEquals(result.getIntents().get("None"), null); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void returnAnswers() { + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + // Set mock response in MockWebServer + String url = "/qnamaker/knowledgebases/"; + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerRecognizer recognizer = new QnAMakerRecognizer(); + recognizer.setHostName(finalEndpoint); + recognizer.setKnowledgeBaseId(knowledgeBaseId); + recognizer.setEndpointKey(endpointKey); + + Activity activity = Activity.createMessageActivity(); + activity.setText("test"); + TurnContext context = new TurnContextImpl(new TestAdapter(), activity); + DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState()); + RecognizerResult result = recognizer.recognize(dc, activity).join(); + validateAnswers(result); + Assert.assertEquals(result.getIntents().get("None"), null); + Assert.assertNotEquals(result.getIntents().get("QnAMatch"), null); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void topNAnswers() { + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_TopNAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + // Set mock response in MockWebServer + String url = "/qnamaker/knowledgebases/"; + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerRecognizer recognizer = new QnAMakerRecognizer(); + recognizer.setHostName(finalEndpoint); + recognizer.setKnowledgeBaseId(knowledgeBaseId); + recognizer.setEndpointKey(endpointKey); + + Activity activity = Activity.createMessageActivity(); + activity.setText("test"); + TurnContext context = new TurnContextImpl(new TestAdapter(), activity); + DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState()); + RecognizerResult result = recognizer.recognize(dc, activity).join(); + validateAnswers(result); + Assert.assertEquals(result.getIntents().get("None"), null); + Assert.assertNotEquals(result.getIntents().get("QnAMatch"), null); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void returnAnswersWithIntents() { + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswerWithIntent.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + // Set mock response in MockWebServer + String url = "/qnamaker/knowledgebases/"; + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerRecognizer recognizer = new QnAMakerRecognizer(); + recognizer.setHostName(finalEndpoint); + recognizer.setKnowledgeBaseId(knowledgeBaseId); + recognizer.setEndpointKey(endpointKey); + + Activity activity = Activity.createMessageActivity(); + activity.setText("test"); + TurnContext context = new TurnContextImpl(new TestAdapter(), activity); + DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState()); + RecognizerResult result = recognizer.recognize(dc, activity).join(); + validateAnswers(result); + Assert.assertEquals(result.getIntents().get("None"), null); + Assert.assertNotEquals(result.getIntents().get("DeferToRecognizer_xxx"), null); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + private String readFileContent (String fileName) throws IOException { + String path = Paths.get("", "src", "test", "java", "com", "microsoft", "bot", "ai", "qna", + "testdata", fileName).toAbsolutePath().toString(); + File file = new File(path); + return FileUtils.readFileToString(file, "utf-8"); + } + + private HttpUrl initializeMockServer(MockWebServer mockWebServer, JsonNode response, String url) throws IOException { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + String mockResponse = mapper.writeValueAsString(response); + mockWebServer.enqueue(new MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .setBody(mockResponse)); + + mockWebServer.start(); + return mockWebServer.url(url); + } + + private void validateAnswers(RecognizerResult result) { + Assert.assertNotEquals(result.getProperties().get("answers"), null); + Assert.assertEquals(result.getEntities().get("answer").size(), 1); + Assert.assertEquals(result.getEntities().get("$instance").get("answer").get(0).get("startIndex").asInt(), 0); + Assert.assertTrue(result.getEntities().get("$instance").get("answer").get(0).get("endIndex") != null); + } +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTests.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTests.java new file mode 100644 index 000000000..c4ca798cc --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTests.java @@ -0,0 +1,2073 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.ai.qna.dialogs.QnAMakerDialog; +import com.microsoft.bot.ai.qna.models.FeedbackRecord; +import com.microsoft.bot.ai.qna.models.FeedbackRecords; +import com.microsoft.bot.ai.qna.models.Metadata; +import com.microsoft.bot.ai.qna.models.QnAMakerTraceInfo; +import com.microsoft.bot.ai.qna.models.QnARequestContext; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.ai.qna.models.QueryResults; +import com.microsoft.bot.ai.qna.utils.QnATelemetryConstants; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MemoryStorage; +import com.microsoft.bot.builder.MemoryTranscriptStore; +import com.microsoft.bot.builder.PagedResult; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.TraceTranscriptLogger; +import com.microsoft.bot.builder.TranscriptLoggerMiddleware; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; + +import com.microsoft.bot.dialogs.ComponentDialog; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogDependencies; +import com.microsoft.bot.dialogs.DialogManager; +import com.microsoft.bot.dialogs.DialogReason; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.LoggerFactory; + +import okhttp3.OkHttpClient; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class QnAMakerTests { + private final String knowledgeBaseId = "dummy-id"; + private final String endpointKey = "dummy-key"; + private final String hostname = "http://localhost"; + private final Boolean mockQnAResponse = true; + + @Captor + ArgumentCaptor eventNameCaptor; + + @Captor + ArgumentCaptor> propertiesCaptor; + + @Captor + ArgumentCaptor> metricsCaptor; + + private String getRequestUrl() { + return String.format("/qnamaker/knowledgebases/%s/generateanswer", knowledgeBaseId); + } + + private String getV2LegacyRequestUrl() { + return String.format("/qnamaker/v2.0/knowledgebases/%s/generateanswer", knowledgeBaseId); + } + + private String getV3LegacyRequestUrl() { + return String.format("/qnamaker/v3.0/knowledgebases/%s/generateanswer", knowledgeBaseId); + } + + private String getTrainRequestUrl() { + return String.format("/qnamaker/v3.0/knowledgebases/%s/train", knowledgeBaseId); + } + + @Test + public void qnaMakerTraceActivity() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + // Invoke flow which uses mock + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("QnaMaker_TraceActivity", "User1", "Bot")) + .use(new TranscriptLoggerMiddleware(transcriptStore)); + final String[] conversationId = {null}; + new TestFlow(adapter, turnContext -> { + // Simulate Qna Lookup + if(turnContext.getActivity().getText().compareTo("how do I clean the stove?") == 0) { + QueryResult[] results = qna.getAnswers(turnContext, null).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", results[0].getAnswer()); + } + delay(500); + conversationId[0] = turnContext.getActivity().getConversation().getId(); + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + turnContext.sendActivity(typingActivity).join(); + delay(500); + turnContext.sendActivity(String.format("echo:%s", turnContext.getActivity().getText())).join(); + return CompletableFuture.completedFuture(null); + }) + .send("how do I clean the stove?") + .assertReply(activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.TYPING)); + }) + .assertReply("echo:how do I clean the stove?") + .delay(500) // This delay avoids the assert immediately above occasionally getting "bar". + .send("bar") + .assertReply(activity -> Assert.assertTrue(activity.isType(ActivityTypes.TYPING))) + .assertReply("echo:bar") + .startTest().join(); + + // Validate Trace Activity created + PagedResult pagedResult = transcriptStore.getTranscriptActivities("test", conversationId[0]).join(); + Assert.assertEquals(7, pagedResult.getItems().size()); + Assert.assertEquals("how do I clean the stove?", pagedResult.getItems().get(0).getText()); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.TRACE)); + QnAMakerTraceInfo traceInfo = (QnAMakerTraceInfo) pagedResult.getItems().get(1).getValue(); + Assert.assertNotNull(traceInfo); + Assert.assertEquals("echo:how do I clean the stove?", pagedResult.getItems().get(3).getText()); + Assert.assertEquals("bar", pagedResult.getItems().get(4).getText()); + Assert.assertEquals("echo:bar", pagedResult.getItems().get(6).getText()); + for (Activity activity : pagedResult.getItems()) { + Assert.assertFalse(StringUtils.isBlank(activity.getId())); + } + } catch(Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTraceActivityEmptyText() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + // Get basic Qna + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + // No text + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("QnaMaker_TraceActivity_EmptyText", "User1", "Bot")); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(new String()); + activity.setConversation(new ConversationAccount()); + activity.setRecipient(new ChannelAccount()); + activity.setFrom(new ChannelAccount()); + + TurnContext context = new TurnContextImpl(adapter, activity); + Assert.assertThrows(CompletionException.class, () -> qna.getAnswers(context, null).join()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTraceActivityNullText() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + // Get basic Qna + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + // No text + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("QnaMaker_TraceActivity_NullText", "User1", "Bot")); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(null); + activity.setConversation(new ConversationAccount()); + activity.setRecipient(new ChannelAccount()); + activity.setFrom(new ChannelAccount()); + + TurnContext context = new TurnContextImpl(adapter, activity); + Assert.assertThrows(CompletionException.class, () -> qna.getAnswers(context, null).join()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTraceActivityNullContext() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + // Get basic Qna + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + Assert.assertThrows(CompletionException.class, () -> qna.getAnswers(null, null).join()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTraceActivityBadMessage() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + // Get basic Qna + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + // No text + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("QnaMaker_TraceActivity_BadMessage", "User1", "Bot")); + Activity activity = new Activity(ActivityTypes.TRACE); + activity.setText("My Text"); + activity.setConversation(new ConversationAccount()); + activity.setRecipient(new ChannelAccount()); + activity.setFrom(new ChannelAccount()); + + TurnContext context = new TurnContextImpl(adapter, activity); + Assert.assertThrows(CompletionException.class, () -> qna.getAnswers(context, null).join()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTraceActivityNullActivity() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + // Get basic Qna + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + // No text + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("QnaMaker_TraceActivity_NullActivity", "User1", "Bot")); + TurnContext context = new MyTurnContext(adapter, null); + Assert.assertThrows(CompletionException.class, () -> qna.getAnswers(context, null).join()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnsAnswer() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", results[0].getAnswer()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnsAnswerRaw() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + QueryResults results = qna.getAnswersRaw(getContext("how do I clean the stove?"), options, null, null).join(); + Assert.assertNotNull(results.getAnswers()); + Assert.assertTrue(results.getActiveLearningEnabled()); + Assert.assertTrue(results.getAnswers().length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results.getAnswers()[0].getAnswer()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerLowScoreVariation() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_TopNAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnaMakerEndpoint = new QnAMakerEndpoint(); + qnaMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnaMakerEndpoint.setEndpointKey(endpointKey); + qnaMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + qnaMakerOptions.setTop(5); + QnAMaker qna = new QnAMaker(qnaMakerEndpoint, qnaMakerOptions); + QueryResult[] results = qna.getAnswers(getContext("Q11"), null).join(); + Assert.assertNotNull(results); + Assert.assertEquals(4, results.length); + + QueryResult[] filteredResults = qna.getLowScoreVariation(results); + Assert.assertNotNull(filteredResults); + Assert.assertEquals(3, filteredResults.length); + + String content2 = readFileContent("QnaMaker_TopNAnswer_DisableActiveLearning.json"); + JsonNode response2 = mapper.readTree(content2); + this.initializeMockServer(mockWebServer, response2, this.getRequestUrl()); + QueryResult[] results2 = qna.getAnswers(getContext("Q11"), null).join(); + Assert.assertNotNull(results2); + Assert.assertEquals(4, results2.length); + + QueryResult[] filteredResults2 = qna.getLowScoreVariation(results2); + Assert.assertNotNull(filteredResults2); + Assert.assertEquals(3, filteredResults2.length); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerCallTrain() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + String url = this.getTrainRequestUrl(); + String endpoint = ""; + try { + JsonNode response = objectMapper.readTree("{}"); + endpoint = String.format( + "%s:%s", + hostname, + initializeMockServer( + mockWebServer, + response, + url).port()); + String finalEndpoint = endpoint; + QnAMakerEndpoint qnaMakerEndpoint = new QnAMakerEndpoint(); + qnaMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnaMakerEndpoint.setEndpointKey(endpointKey); + qnaMakerEndpoint.setHost(finalEndpoint); + + QnAMaker qna = new QnAMaker(qnaMakerEndpoint, null); + FeedbackRecords feedbackRecords = new FeedbackRecords(); + + FeedbackRecord feedback1 = new FeedbackRecord(); + feedback1.setQnaId(1); + feedback1.setUserId("test"); + feedback1.setUserQuestion("How are you?"); + + FeedbackRecord feedback2 = new FeedbackRecord(); + feedback2.setQnaId(2); + feedback2.setUserId("test"); + feedback2.setUserQuestion("What up??"); + + feedbackRecords.setRecords(new FeedbackRecord[] { feedback1, feedback2 }); + qna.callTrain(feedbackRecords); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnsAnswerConfiguration() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results[0].getAnswer()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnsAnswerWithFiltering() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_UsesStrictFilters_ToReturnAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnaMakerEndpoint = new QnAMakerEndpoint(); + qnaMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnaMakerEndpoint.setEndpointKey(endpointKey); + qnaMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + Metadata metadata = new Metadata(); + metadata.setName("topic"); + metadata.setValue("value"); + Metadata[] filters = new Metadata[] { metadata }; + qnaMakerOptions.setStrictFilters(filters); + qnaMakerOptions.setTop(1); + QnAMaker qna = new QnAMaker(qnaMakerEndpoint, qnaMakerOptions); + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), qnaMakerOptions).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results[0].getAnswer()); + Assert.assertEquals("topic", results[0].getMetadata()[0].getName()); + Assert.assertEquals("value", results[0].getMetadata()[0].getValue()); + + JsonNode obj = null; + try { + RecordedRequest request = mockWebServer.takeRequest(); + obj = objectMapper.readTree(request.getBody().readUtf8()); + } catch (IOException | InterruptedException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + // verify we are actually passing on the options + Assert.assertEquals(1, obj.get("top").asInt()); + Assert.assertEquals("topic", obj.get("strictFilters").get(0).get("name").asText()); + Assert.assertEquals("value", obj.get("strictFilters").get(0).get("value").asText()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerSetScoreThresholdWhenThresholdIsZero() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnaMakerEndpoint = new QnAMakerEndpoint(); + qnaMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnaMakerEndpoint.setEndpointKey(endpointKey); + qnaMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + qnaMakerOptions.setScoreThreshold(0.0f); + + QnAMaker qnaWithZeroValueThreshold = new QnAMaker(qnaMakerEndpoint, qnaMakerOptions); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + QueryResult[] results = qnaWithZeroValueThreshold.getAnswers(getContext("how do I clean the stove?"), options).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTestThreshold() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_TestThreshold.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + qnaMakerOptions.setTop(1); + qnaMakerOptions.setScoreThreshold(0.99F); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, qnaMakerOptions); + + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTestScoreThresholdTooLargeOutOfRange() { + PrintMethodName(); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(hostname); + + QnAMakerOptions tooLargeThreshold = new QnAMakerOptions(); + tooLargeThreshold.setTop(1); + tooLargeThreshold.setScoreThreshold(1.1f); + + Assert.assertThrows(IllegalArgumentException.class, () -> new QnAMaker(qnAMakerEndpoint, tooLargeThreshold)); + } + + @Test + public void qnaMakerTestScoreThresholdTooSmallOutOfRange() { + PrintMethodName(); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(hostname); + + QnAMakerOptions tooSmallThreshold = new QnAMakerOptions(); + tooSmallThreshold.setTop(1); + tooSmallThreshold.setScoreThreshold(-9000.0f); + + Assert.assertThrows(IllegalArgumentException.class, () -> new QnAMaker(qnAMakerEndpoint, tooSmallThreshold)); + } + + @Test + public void qnaMakerReturnsAnswerWithContext() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswerWithContext.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnARequestContext context = new QnARequestContext(); + context.setPreviousQnAId(5); + context.setPreviousUserQuery("how do I clean the stove?"); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + options.setContext(context); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options); + + QueryResult[] results = qna.getAnswers(getContext("Where can I buy?"), options).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals(55, (int)results[0].getId()); + Assert.assertEquals(1, (double)results[0].getScore(), 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnAnswersWithoutContext() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswerWithoutContext.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(3); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options); + + QueryResult[] results = qna.getAnswers(getContext("Where can I buy?"), options).join(); + Assert.assertNotNull(results); + Assert.assertEquals(2, results.length); + Assert.assertNotEquals(1, results[0].getScore().intValue()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnsHighScoreWhenIdPassed() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswerWithContext.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + options.setQnAId(55); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options); + QueryResult[] results = qna.getAnswers(getContext("Where can I buy?"), options).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals(55, (int)results[0].getId()); + Assert.assertEquals(1, (double)results[0].getScore(), 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTestTopOutOfRange() { + PrintMethodName(); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(hostname); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(-1); + options.setScoreThreshold(0.5f); + + Assert.assertThrows(IllegalArgumentException.class, () -> new QnAMaker(qnAMakerEndpoint, options)); + } + + @Test + public void qnaMakerTestEndpointEmptyKbId() { + PrintMethodName(); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(new String()); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(hostname); + + Assert.assertThrows(IllegalArgumentException.class, () -> new QnAMaker(qnAMakerEndpoint, null)); + } + + @Test + public void qnaMakerTestEndpointEmptyEndpointKey() { + PrintMethodName(); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(new String()); + qnAMakerEndpoint.setHost(hostname); + + Assert.assertThrows(IllegalArgumentException.class, () -> new QnAMaker(qnAMakerEndpoint, null)); + } + + @Test + public void qnaMakerTestEndpointEmptyHost() { + PrintMethodName(); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(new String()); + + Assert.assertThrows(IllegalArgumentException.class, () -> new QnAMaker(qnAMakerEndpoint, null)); + } + + @Test + public void qnaMakerUserAgent() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + QnAMaker qna = this.qnaReturnsAnswer(mockWebServer); + + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + RecordedRequest request = mockWebServer.takeRequest(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results[0].getAnswer()); + + // Verify that we added the bot.builder package details. + Assert.assertTrue(request.getHeader("User-Agent").contains("BotBuilder/4.")); + } catch (Exception ex) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerV2LegacyEndpointShouldThrow() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_LegacyEndpointAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getV2LegacyRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String host = String.format("{%s}/v2.0", endpoint); + QnAMakerEndpoint v2LegacyEndpoint = new QnAMakerEndpoint(); + v2LegacyEndpoint.setKnowledgeBaseId(knowledgeBaseId); + v2LegacyEndpoint.setEndpointKey(endpointKey); + v2LegacyEndpoint.setHost(host); + + Assert.assertThrows(UnsupportedOperationException.class, + () -> new QnAMaker(v2LegacyEndpoint,null)); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerV3LeagacyEndpointShouldThrow() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_LegacyEndpointAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getV3LegacyRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String host = String.format("{%s}/v3.0", endpoint); + QnAMakerEndpoint v3LegacyEndpoint = new QnAMakerEndpoint(); + v3LegacyEndpoint.setKnowledgeBaseId(knowledgeBaseId); + v3LegacyEndpoint.setEndpointKey(endpointKey); + v3LegacyEndpoint.setHost(host); + + Assert.assertThrows(UnsupportedOperationException.class, + () -> new QnAMaker(v3LegacyEndpoint,null)); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerReturnsAnswerWithMetadataBoost() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswersWithMetadataBoost.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options); + + QueryResult[] results = qna.getAnswers(getContext("who loves me?"), options).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("Kiki", results[0].getAnswer()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTestThresholdInQueryOption() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer_GivenScoreThresholdQueryOption.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions queryOptionsWithScoreThreshold = new QnAMakerOptions(); + queryOptionsWithScoreThreshold.setScoreThreshold(0.5f); + queryOptionsWithScoreThreshold.setTop(2); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, queryOptionsWithScoreThreshold); + + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + + QueryResult[] results = qna.getAnswers(getContext("What happens when you hug a porcupine?"), queryOptionsWithScoreThreshold).join(); + RecordedRequest request = mockWebServer.takeRequest(); + JsonNode obj = objectMapper.readTree(request.getBody().readUtf8()); + + Assert.assertNotNull(results); + + Assert.assertEquals(2, obj.get("top").asInt()); + Assert.assertEquals(0.5, obj.get("scoreThreshold").asDouble(), 0); + + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTestUnsuccessfulResponse() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse().setResponseCode(502)); + try { + String url = this.getRequestUrl(); + String finalEndpoint = String.format("%s:%s", hostname, mockWebServer.url(url).port()); + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, null); + Assert.assertThrows(CompletionException.class, () -> qna.getAnswers(getContext("how do I clean the stove?"), null).join()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerIsTestTrue() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_IsTest_True.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + qnaMakerOptions.setTop(1); + qnaMakerOptions.setIsTest(true); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, qnaMakerOptions); + + QueryResult[] results = qna.getAnswers(getContext("Q11"), qnaMakerOptions).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerRankerTypeQuestionOnly() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_RankerType_QuestionOnly.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + qnaMakerOptions.setTop(1); + qnaMakerOptions.setRankerType("QuestionOnly"); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, qnaMakerOptions); + + QueryResult[] results = qna.getAnswers(getContext("Q11"), qnaMakerOptions).join(); + Assert.assertNotNull(results); + Assert.assertEquals(2, results.length); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerTestOptionsHydration() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String url = this.getRequestUrl(); + String endpoint = ""; + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + + QnAMakerOptions noFiltersOptions = new QnAMakerOptions(); + noFiltersOptions.setTop(30); + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + Metadata strictFilterMovie = new Metadata(); + strictFilterMovie.setName("movie"); + strictFilterMovie.setValue("disney"); + + Metadata strictFilterHome = new Metadata(); + strictFilterHome.setName("home"); + strictFilterHome.setValue("floating"); + + Metadata strictFilterDog = new Metadata(); + strictFilterDog.setName("dog"); + strictFilterDog.setValue("samoyed"); + + Metadata[] oneStrictFilters = new Metadata[] {strictFilterMovie}; + Metadata[] twoStrictFilters = new Metadata[] {strictFilterMovie, strictFilterHome}; + Metadata[] allChangedRequestOptionsFilters = new Metadata[] {strictFilterDog}; + QnAMakerOptions oneFilteredOption = new QnAMakerOptions(); + oneFilteredOption.setTop(30); + oneFilteredOption.setStrictFilters(oneStrictFilters); + + QnAMakerOptions twoStrictFiltersOptions = new QnAMakerOptions(); + twoStrictFiltersOptions.setTop(30); + twoStrictFiltersOptions.setStrictFilters(twoStrictFilters); + + QnAMakerOptions allChangedRequestOptions = new QnAMakerOptions(); + allChangedRequestOptions.setTop(2000); + allChangedRequestOptions.setScoreThreshold(0.42f); + allChangedRequestOptions.setStrictFilters(allChangedRequestOptionsFilters); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, noFiltersOptions); + + TurnContext context = getContext("up"); + + // Ensure that options from previous requests do not bleed over to the next, + // And that the options set in the constructor are not overwritten improperly by options passed into .GetAnswersAsync() + CapturedRequest[] requestContent = new CapturedRequest[6]; + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + RecordedRequest request; + + qna.getAnswers(context, noFiltersOptions).join(); + request = mockWebServer.takeRequest(); + requestContent[0] = objectMapper.readValue(request.getBody().readUtf8(), CapturedRequest.class); + + this.enqueueResponse(mockWebServer, response); + + qna.getAnswers(context, twoStrictFiltersOptions).join(); + request = mockWebServer.takeRequest(); + requestContent[1] = objectMapper.readValue(request.getBody().readUtf8(), CapturedRequest.class); + + this.enqueueResponse(mockWebServer, response); + + qna.getAnswers(context, oneFilteredOption).join(); + request = mockWebServer.takeRequest(); + requestContent[2] = objectMapper.readValue(request.getBody().readUtf8(), CapturedRequest.class); + + this.enqueueResponse(mockWebServer, response); + + qna.getAnswers(context, null).join(); + request = mockWebServer.takeRequest(); + requestContent[3] = objectMapper.readValue(request.getBody().readUtf8(), CapturedRequest.class); + + this.enqueueResponse(mockWebServer, response); + + qna.getAnswers(context, allChangedRequestOptions).join(); + request = mockWebServer.takeRequest(); + requestContent[4] = objectMapper.readValue(request.getBody().readUtf8(), CapturedRequest.class); + + this.enqueueResponse(mockWebServer, response); + + qna.getAnswers(context, null).join(); + request = mockWebServer.takeRequest(); + requestContent[5] = objectMapper.readValue(request.getBody().readUtf8(), CapturedRequest.class); + + + Assert.assertTrue(requestContent[0].getStrictFilters().length == 0); + Assert.assertEquals(2, requestContent[1].getStrictFilters().length); + Assert.assertTrue(requestContent[2].getStrictFilters().length == 1); + Assert.assertTrue(requestContent[3].getStrictFilters().length == 0); + + Assert.assertEquals(2000, requestContent[4].getTop().intValue()); + Assert.assertEquals(0.42, Math.round(requestContent[4].getScoreThreshold().doubleValue()), 1); + Assert.assertTrue(requestContent[4].getStrictFilters().length == 1); + + Assert.assertEquals(30, requestContent[5].getTop().intValue()); + Assert.assertEquals(0.3, Math.round(requestContent[5].getScoreThreshold().doubleValue()),1); + Assert.assertTrue(requestContent[5].getStrictFilters().length == 0); + + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + LoggerFactory.getLogger(QnAMakerTests.class).error(e.getMessage()); + } + } + } + + @Test + public void qnaMakerStrictFiltersCompoundOperationType() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + Metadata strictFilterMovie = new Metadata(); + strictFilterMovie.setName("movie"); + strictFilterMovie.setValue("disney"); + + Metadata strictFilterProduction = new Metadata(); + strictFilterProduction.setName("production"); + strictFilterProduction.setValue("Walden"); + + Metadata[] strictFilters = new Metadata[] {strictFilterMovie, strictFilterProduction}; + QnAMakerOptions oneFilteredOption = new QnAMakerOptions(); + oneFilteredOption.setTop(30); + oneFilteredOption.setStrictFilters(strictFilters); + oneFilteredOption.setStrictFiltersJoinOperator(JoinOperator.OR); + + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, oneFilteredOption); + + TurnContext context = getContext("up"); + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + + QueryResult[] noFilterResults1 = qna.getAnswers(context, oneFilteredOption).join(); + RecordedRequest request = mockWebServer.takeRequest(); + JsonNode requestContent = objectMapper.readTree(request.getBody().readUtf8()); + Assert.assertEquals(2, oneFilteredOption.getStrictFilters().length); + Assert.assertEquals(JoinOperator.OR, oneFilteredOption.getStrictFiltersJoinOperator()); + }catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryNullTelemetryClient() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + // Act (Null Telemetry client) + // This will default to the NullTelemetryClient which no-ops all calls. + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options, null, true); + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", results[0].getAnswer()); + Assert.assertEquals("Editorial", results[0].getSource()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryReturnsAnswer() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act - See if we get data back in telemetry + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options, telemetryClient, true); + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + // Assert - Check Telemetry logged + // verify BotTelemetryClient was invoked 1 times, and capture arguments. + verify(telemetryClient, times(1)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture(), + metricsCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + List> metrics = metricsCaptor.getAllValues(); ; + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertTrue(properties.get(0).containsKey("knowledgeBaseId")); + Assert.assertTrue(properties.get(0).containsKey("matchedQuestion")); + Assert.assertTrue(properties.get(0).containsKey("question")); + Assert.assertTrue(properties.get(0).containsKey("questionId")); + Assert.assertTrue(properties.get(0).containsKey("answer")); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", properties.get(0).get("answer")); + Assert.assertTrue(properties.get(0).containsKey("articleFound")); + Assert.assertTrue(metrics.get(0).size() == 1); + + // Assert - Validate we didn't break QnA functionality. + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", results[0].getAnswer()); + Assert.assertEquals("Editorial", results[0].getSource()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryReturnsAnswerWhenNoAnswerFoundInKB() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer_WhenNoAnswerFoundInKb.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act - See if we get data back in telemetry + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options, telemetryClient, true); + QueryResult[] results = qna.getAnswers(getContext("what is the answer to my nonsense question?"), null).join(); + // Assert - Check Telemetry logged + // verify BotTelemetryClient was invoked 1 times, and capture arguments. + verify(telemetryClient, times(1)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture(), + metricsCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + List> metrics = metricsCaptor.getAllValues(); + + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertTrue(properties.get(0).containsKey("knowledgeBaseId")); + Assert.assertTrue(properties.get(0).containsKey("matchedQuestion")); + Assert.assertEquals("No Qna Question matched", properties.get(0).get("matchedQuestion")); + Assert.assertTrue(properties.get(0).containsKey("question")); + Assert.assertTrue(properties.get(0).containsKey("questionId")); + Assert.assertTrue(properties.get(0).containsKey("answer")); + Assert.assertEquals("No Qna Answer matched", properties.get(0).get("answer")); + Assert.assertTrue(properties.get(0).containsKey("articleFound")); + Assert.assertTrue(metrics.get(0).isEmpty()); + + // Assert - Validate we didn't break QnA functionality. + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryPii() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options, telemetryClient, false); + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null).join(); + // verify BotTelemetryClient was invoked 1 times, and capture arguments. + verify(telemetryClient, times(1)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture(), + metricsCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + List> metrics = metricsCaptor.getAllValues(); + + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertTrue(properties.get(0).containsKey("knowledgeBaseId")); + Assert.assertTrue(properties.get(0).containsKey("matchedQuestion")); + Assert.assertFalse(properties.get(0).containsKey("question")); + Assert.assertTrue(properties.get(0).containsKey("questionId")); + Assert.assertTrue(properties.get(0).containsKey("answer")); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + properties.get(0).get("answer")); + Assert.assertTrue(properties.get(0).containsKey("articleFound")); + Assert.assertTrue(metrics.get(0).size() == 1); + Assert.assertTrue(metrics.get(0).containsKey("score")); + + // Assert - Validate we didn't break QnA functionality. + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results[0].getAnswer()); + Assert.assertEquals("Editorial", results[0].getSource()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryOverride() { + PrintMethodName(); + + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act - Override the QnaMaker object to log custom stuff and honor parms passed in. + Map telemetryProperties = new HashMap(); + telemetryProperties.put("Id", "MyID"); + + QnAMaker qna = new OverrideTelemetry(qnAMakerEndpoint, options, telemetryClient, false); + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null, telemetryProperties, null).join(); + + // verify BotTelemetryClient was invoked 2 times, and capture arguments. + verify(telemetryClient, times(2)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(2, eventNames.size()); + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertTrue(properties.get(0).size() == 2); + Assert.assertTrue(properties.get(0).containsKey("MyImportantProperty")); + Assert.assertEquals("myImportantValue", properties.get(0).get("MyImportantProperty")); + Assert.assertTrue(properties.get(0).containsKey("Id")); + Assert.assertEquals("MyID", properties.get(0).get("Id")); + + Assert.assertEquals("MySecondEvent", eventNames.get(1)); + Assert.assertTrue(properties.get(1).containsKey("MyImportantProperty2")); + Assert.assertEquals("myImportantValue2", properties.get(1).get("MyImportantProperty2")); + + // Validate we didn't break QnA functionality. + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results[0].getAnswer()); + Assert.assertEquals("Editorial", results[0].getSource()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryAdditionalPropsMetrics() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act - Pass in properties during QnA invocation + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options, telemetryClient, false); + Map telemetryProperties = new HashMap(); + telemetryProperties.put("MyImportantProperty", "myImportantValue"); + + Map telemetryMetrics = new HashMap(); + telemetryMetrics.put("MyImportantMetric", 3.14159); + + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null, telemetryProperties, telemetryMetrics).join(); + // Assert - added properties were added. + // verify BotTelemetryClient was invoked 1 times, and capture arguments. + verify(telemetryClient, times(1)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture(), + metricsCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + List> metrics = metricsCaptor.getAllValues(); + + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertTrue(properties.get(0).containsKey(QnATelemetryConstants.KNOWLEDGE_BASE_ID_PROPERTY)); + Assert.assertFalse(properties.get(0).containsKey(QnATelemetryConstants.QUESTION_PROPERTY)); + Assert.assertTrue(properties.get(0).containsKey(QnATelemetryConstants.MATCHED_QUESTION_PROPERTY)); + Assert.assertTrue(properties.get(0).containsKey(QnATelemetryConstants.QUESTION_ID_PROPERTY)); + Assert.assertTrue(properties.get(0).containsKey(QnATelemetryConstants.ANSWER_PROPERTY)); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + properties.get(0).get("answer")); + Assert.assertTrue(properties.get(0).containsKey("MyImportantProperty")); + Assert.assertEquals("myImportantValue", properties.get(0).get("MyImportantProperty")); + + Assert.assertEquals(2, metrics.get(0).size()); + Assert.assertTrue(metrics.get(0).containsKey("score")); + Assert.assertTrue(metrics.get(0).containsKey("MyImportantMetric")); + Assert.assertTrue(Double.compare((double)metrics.get(0).get("MyImportantMetric"), 3.14159) == 0); + + // Validate we didn't break QnA functionality. + Assert.assertNotNull(results); + Assert.assertTrue(results.length == 1); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + results[0].getAnswer()); + Assert.assertEquals("Editorial", results[0].getSource()); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryAdditionalPropsOverride() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act - Pass in properties during QnA invocation that override default properties + // NOTE: We are invoking this with PII turned OFF, and passing a PII property (originalQuestion). + QnAMaker qna = new QnAMaker(qnAMakerEndpoint, options, telemetryClient, false); + Map telemetryProperties = new HashMap(); + telemetryProperties.put("knowledgeBaseId", "myImportantValue"); + telemetryProperties.put("originalQuestion", "myImportantValue2"); + + Map telemetryMetrics = new HashMap(); + telemetryMetrics.put("score", 3.14159); + + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null, telemetryProperties, telemetryMetrics).join(); + // Assert - added properties were added. + // verify BotTelemetryClient was invoked 1 times, and capture arguments. + verify(telemetryClient, times(1)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture(), + metricsCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + List> metrics = metricsCaptor.getAllValues(); + + Assert.assertEquals(1, eventNames.size()); + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertTrue(properties.get(0).containsKey("knowledgeBaseId")); + Assert.assertEquals("myImportantValue", properties.get(0).get("knowledgeBaseId")); + Assert.assertTrue(properties.get(0).containsKey("matchedQuestion")); + Assert.assertEquals("myImportantValue2", properties.get(0).get("originalQuestion")); + Assert.assertFalse(properties.get(0).containsKey("question")); + Assert.assertTrue(properties.get(0).containsKey("questionId")); + Assert.assertTrue(properties.get(0).containsKey("answer")); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + properties.get(0).get("answer")); + Assert.assertFalse(properties.get(0).containsKey("MyImportantProperty")); + + Assert.assertEquals(1, metrics.get(0).size()); + Assert.assertTrue(metrics.get(0).containsKey("score")); + Assert.assertTrue(Double.compare((double)metrics.get(0).get("score"), 3.14159) == 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + @Test + public void telemetryFillPropsOverride() { + PrintMethodName(); + + // Arrange + MockWebServer mockWebServer = new MockWebServer(); + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer, response, url).port()); + } + String finalEndpoint = endpoint; + QnAMakerEndpoint qnAMakerEndpoint = new QnAMakerEndpoint(); + qnAMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnAMakerEndpoint.setEndpointKey(endpointKey); + qnAMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions options = new QnAMakerOptions(); + options.setTop(1); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + + // Act - Pass in properties during QnA invocation that override default properties + // In addition Override with derivation. This presents an interesting question of order of setting properties. + // If I want to override "originalQuestion" property: + // - Set in "Stock" schema + // - Set in derived QnAMaker class + // - Set in GetAnswersAsync + // Logically, the GetAnswersAync should win. But ultimately OnQnaResultsAsync decides since it is the last + // code to touch the properties before logging (since it actually logs the event). + QnAMaker qna = new OverrideFillTelemetry(qnAMakerEndpoint, options, telemetryClient, false); + Map telemetryProperties = new HashMap(); + telemetryProperties.put("knowledgeBaseId", "myImportantValue"); + telemetryProperties.put("matchedQuestion", "myImportantValue2"); + + Map telemetryMetrics = new HashMap(); + telemetryMetrics.put("score", 3.14159); + + QueryResult[] results = qna.getAnswers(getContext("how do I clean the stove?"), null, telemetryProperties, telemetryMetrics).join(); + // Assert - added properties were added. + // verify BotTelemetryClient was invoked 2 times calling different trackEvents methods, and capture arguments. + verify(telemetryClient, times(1)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture(), + metricsCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + List> metrics = metricsCaptor.getAllValues(); + + Assert.assertEquals(eventNames.get(0), QnATelemetryConstants.QNA_MSG_EVENT); + Assert.assertEquals(6, properties.get(0).size()); + Assert.assertTrue(properties.get(0).containsKey("knowledgeBaseId")); + Assert.assertEquals("myImportantValue", properties.get(0).get("knowledgeBaseId")); + Assert.assertTrue(properties.get(0).containsKey("matchedQuestion")); + Assert.assertEquals("myImportantValue2", properties.get(0).get("matchedQuestion")); + Assert.assertTrue(properties.get(0).containsKey("questionId")); + Assert.assertTrue(properties.get(0).containsKey("answer")); + Assert.assertEquals("BaseCamp: You can use a damp rag to clean around the Power Pack", + properties.get(0).get("answer")); + Assert.assertTrue(properties.get(0).containsKey("articleFound")); + Assert.assertTrue(properties.get(0).containsKey("MyImportantProperty")); + Assert.assertEquals("myImportantValue", properties.get(0).get("MyImportantProperty")); + + Assert.assertEquals(1, metrics.get(0).size()); + Assert.assertTrue(metrics.get(0).containsKey("score")); + Assert.assertTrue(Double.compare((double)metrics.get(0).get("score"), 3.14159) == 0); + } catch (Exception e) { + fail(); + } finally { + try { + mockWebServer.shutdown(); + } catch (IOException e) { + // Empty error + } + } + } + + private void PrintMethodName() + { + System.out.println("Running " + (new Throwable().getStackTrace()[1].getMethodName()) + "()"); + } + + private static TurnContext getContext(String utterance) { + TestAdapter b = new TestAdapter(); + Activity a = new Activity(ActivityTypes.MESSAGE); + a.setText(utterance); + a.setConversation(new ConversationAccount()); + a.setRecipient(new ChannelAccount()); + a.setFrom(new ChannelAccount()); + + return new TurnContextImpl(b, a); + } + + private TestFlow createFlow(Dialog rootDialog, String testName) { + Storage storage = new MemoryStorage(); + UserState userState = new UserState(storage); + ConversationState conversationState = new ConversationState(storage); + + TestAdapter adapter = new TestAdapter(TestAdapter.createConversationReference(testName, "User1", "Bot")); + adapter + .useStorage(storage) + .useBotState(userState, conversationState) + .use(new TranscriptLoggerMiddleware(new TraceTranscriptLogger())); + + DialogManager dm = new DialogManager(rootDialog, null); + return new TestFlow(adapter, (turnContext) -> dm.onTurn(turnContext).thenApply(task -> null)); + } + + public class QnAMakerTestDialog extends ComponentDialog implements DialogDependencies { + + public QnAMakerTestDialog(String knowledgeBaseId, String endpointKey, String hostName, OkHttpClient httpClient) { + super("QnaMakerTestDialog"); + addDialog(new QnAMakerDialog(knowledgeBaseId, endpointKey, hostName, null, + null, null, null, null, + null, null, httpClient)); + } + + @Override + public CompletableFuture beginDialog(DialogContext outerDc, Object options) { + return this.continueDialog(outerDc); + } + + @Override + public CompletableFuture continueDialog(DialogContext dc) { + if (dc.getContext().getActivity().getText() == "moo") { + return dc.getContext().sendActivity("Yippee ki-yay!").thenApply(task -> END_OF_TURN); + } + + return dc.beginDialog("qnaDialog").thenApply(task -> task); + } + + public List getDependencies() { + return getDialogs().getDialogs().stream().collect(Collectors.toList()); + } + + @Override + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + if(!(boolean)result) { + dc.getContext().sendActivity("I didn't understand that."); + } + + return super.resumeDialog(dc, reason, result).thenApply(task -> task); + } + } + + private QnAMaker qnaReturnsAnswer(MockWebServer mockWebServer) { + try { + String content = readFileContent("QnaMaker_ReturnsAnswer.json"); + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode response = mapper.readTree(content); + String url = this.getRequestUrl(); + String endpoint = ""; + if (this.mockQnAResponse) { + endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port()); + } + String finalEndpoint = endpoint; + // Mock Qna + QnAMakerEndpoint qnaMakerEndpoint = new QnAMakerEndpoint(); + qnaMakerEndpoint.setKnowledgeBaseId(knowledgeBaseId); + qnaMakerEndpoint.setEndpointKey(endpointKey); + qnaMakerEndpoint.setHost(finalEndpoint); + + QnAMakerOptions qnaMakerOptions = new QnAMakerOptions(); + qnaMakerOptions.setTop(1); + + return new QnAMaker(qnaMakerEndpoint, qnaMakerOptions); + } catch (Exception e) { + return null; + } + } + + private String readFileContent (String fileName) throws IOException { + String path = Paths.get("", "src", "test", "java", "com", "microsoft", "bot", "ai", "qna", + "testData", fileName).toAbsolutePath().toString(); + File file = new File(path); + return FileUtils.readFileToString(file, "utf-8"); + } + + private HttpUrl initializeMockServer(MockWebServer mockWebServer, JsonNode response, String url) throws IOException { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + String mockResponse = mapper.writeValueAsString(response); + mockWebServer.enqueue(new MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .setBody(mockResponse)); + + try { + mockWebServer.start(); + } catch (Exception e) { + // Empty error + } + return mockWebServer.url(url); + } + + private void enqueueResponse(MockWebServer mockWebServer, JsonNode response) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + String mockResponse = mapper.writeValueAsString(response); + mockWebServer.enqueue(new MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .setBody(mockResponse)); + } + + /** + * Time period delay. + * @param milliseconds Time to delay. + */ + private void delay(int milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public class OverrideTelemetry extends QnAMaker { + + public OverrideTelemetry(QnAMakerEndpoint endpoint, QnAMakerOptions options, + BotTelemetryClient telemetryClient, Boolean logPersonalInformation) { + super(endpoint, options, telemetryClient, logPersonalInformation); + } + + @Override + protected CompletableFuture onQnaResults(QueryResult[] queryResults, TurnContext turnContext, + Map telemetryProperties, + Map telemetryMetrics) { + Map properties = telemetryProperties == null ? new HashMap() : telemetryProperties; + + // GetAnswerAsync overrides derived class. + properties.put("MyImportantProperty", "myImportantValue"); + + // Log event + BotTelemetryClient telemetryClient = getTelemetryClient(); + telemetryClient.trackEvent(QnATelemetryConstants.QNA_MSG_EVENT, properties); + + // Create second event. + Map secondEventProperties = new HashMap(); + secondEventProperties.put("MyImportantProperty2", "myImportantValue2"); + telemetryClient.trackEvent("MySecondEvent", secondEventProperties); + return CompletableFuture.completedFuture(null); + } + + } + + public class OverrideFillTelemetry extends QnAMaker { + + public OverrideFillTelemetry(QnAMakerEndpoint endpoint, QnAMakerOptions options, + BotTelemetryClient telemetryClient, Boolean logPersonalInformation) { + super(endpoint, options, telemetryClient, logPersonalInformation); + } + + @Override + protected CompletableFuture onQnaResults(QueryResult[] queryResults, TurnContext turnContext, + Map telemetryProperties, + Map telemetryMetrics) throws IOException { + return this.fillQnAEvent(queryResults, turnContext, telemetryProperties, telemetryMetrics).thenAccept(eventData -> { + // Add my property + eventData.getLeft().put("MyImportantProperty", "myImportantValue"); + + BotTelemetryClient telemetryClient = this.getTelemetryClient(); + + // Log QnaMessage event + telemetryClient.trackEvent(QnATelemetryConstants.QNA_MSG_EVENT, eventData.getLeft(), eventData.getRight()); + + // Create second event. + Map secondEventProperties = new HashMap(); + secondEventProperties.put("MyImportantProperty2", "myImportantValue2"); + + telemetryClient.trackEvent("MySecondEvent", secondEventProperties); + }); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class CapturedRequest { + private String[] questions; + private Integer top; + private Metadata[] strictFilters; + private Metadata[] MetadataBoost; + private Float scoreThreshold; + + public String[] getQuestions() { + return questions; + } + + public void setQuestions(String[] questions) { + this.questions = questions; + } + + public Integer getTop() { + return top; + } + + public Metadata[] getStrictFilters() { + return strictFilters; + } + + public Float getScoreThreshold() { + return scoreThreshold; + } + } +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTraceInfoTests.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTraceInfoTests.java new file mode 100644 index 000000000..fbc57019e --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTraceInfoTests.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.ai.qna; + +import java.io.IOException; +import java.util.UUID; + +import com.microsoft.bot.ai.qna.models.QnAMakerTraceInfo; +import com.microsoft.bot.ai.qna.models.QueryResult; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; + +import org.junit.Assert; +import org.junit.Test; + +public class QnAMakerTraceInfoTests { + + @Test + public void qnaMakerTraceInfoSerialization() throws IOException { + QueryResult queryResult = new QueryResult(); + queryResult.setQuestions(new String[] { "What's your name?" }); + queryResult.setAnswer("My name is Mike"); + queryResult.setScore(0.9f); + QueryResult[] queryResults = new QueryResult[] { queryResult }; + + QnAMakerTraceInfo qnaMakerTraceInfo = new QnAMakerTraceInfo(); + qnaMakerTraceInfo.setQueryResults(queryResults); + qnaMakerTraceInfo.setKnowledgeBaseId(UUID.randomUUID().toString()); + qnaMakerTraceInfo.setScoreThreshold(0.5f); + qnaMakerTraceInfo.setTop(1); + + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + String serialized = jacksonAdapter.serialize(qnaMakerTraceInfo); + QnAMakerTraceInfo deserialized = jacksonAdapter.deserialize(serialized, QnAMakerTraceInfo.class); + + Assert.assertNotNull(deserialized); + Assert.assertNotNull(deserialized.getQueryResults()); + Assert.assertNotNull(deserialized.getKnowledgeBaseId()); + Assert.assertEquals(0.5, deserialized.getScoreThreshold(), 0); + Assert.assertEquals(1, deserialized.getTop(), 0); + Assert.assertEquals(qnaMakerTraceInfo.getQueryResults()[0].getQuestions()[0], + deserialized.getQueryResults()[0].getQuestions()[0]); + Assert.assertEquals(qnaMakerTraceInfo.getQueryResults()[0].getAnswer(), + deserialized.getQueryResults()[0].getAnswer()); + Assert.assertEquals(qnaMakerTraceInfo.getKnowledgeBaseId(), deserialized.getKnowledgeBaseId()); + Assert.assertEquals(qnaMakerTraceInfo.getScoreThreshold(), deserialized.getScoreThreshold()); + Assert.assertEquals(qnaMakerTraceInfo.getTop(), deserialized.getTop()); + } +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_IsTest_True.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_IsTest_True.json new file mode 100644 index 000000000..263f6a1e5 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_IsTest_True.json @@ -0,0 +1,13 @@ +{ + "activeLearningEnabled": true, + "answers": [ + { + "questions": [], + "answer": "No good match found in KB.", + "score": 0, + "id": -1, + "source": null, + "metadata": [] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_LegacyEndpointAnswer.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_LegacyEndpointAnswer.json new file mode 100644 index 000000000..158877934 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_LegacyEndpointAnswer.json @@ -0,0 +1,13 @@ +{ + "answers": [ + { + "score": 30.500827898, + "qnaId": 18, + "answer": "To be the very best, you gotta catch 'em all", + "source": "Custom Editorial", + "questions": [ + "How do I be the best?" + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_RankerType_QuestionOnly.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_RankerType_QuestionOnly.json new file mode 100644 index 000000000..ff2b0f788 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_RankerType_QuestionOnly.json @@ -0,0 +1,35 @@ +{ + "activeLearningEnabled": false, + "answers": [ + { + "questions": [ + "Q1" + ], + "answer": "A1", + "score": 80, + "id": 15, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q2" + ], + "answer": "A2", + "score": 78, + "id": 16, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnAnswer_MultiTurnLevel1.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnAnswer_MultiTurnLevel1.json new file mode 100644 index 000000000..48f461826 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnAnswer_MultiTurnLevel1.json @@ -0,0 +1,19 @@ +{ + "answers": [ + { + "questions": [ + "I accidentally deleted a part of my QnA Maker, what should I do?" + ], + "answer": "All deletes are permanent, including question and answer pairs, files, URLs, custom questions and answers, knowledge bases, or Azure resources. Make sure you export your knowledge base from the Settings**page before deleting any part of your knowledge base.", + "score": 89.99, + "id": 1, + "source": "https://docs.microsoft.com/en-us/azure/cognitive-services/qnamaker/troubleshooting", + "metadata": [], + "context": { + "isContextOnly": false, + "prompts": [] + } + } + ], + "activeLearningEnabled": false +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnAnswer_withPrompts.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnAnswer_withPrompts.json new file mode 100644 index 000000000..67f5ce464 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnAnswer_withPrompts.json @@ -0,0 +1,38 @@ +{ + "activeLearningEnabled": true, + "answers": [ + { + "questions": [ + "Issues related to KB" + ], + "answer": "Please select one of the following KB issues. ", + "score": 98.0, + "id": 27, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": false, + "prompts": [ + { + "displayOrder": 0, + "qnaId": 1, + "qna": null, + "displayText": "Accidently deleted KB" + }, + { + "displayOrder": 0, + "qnaId": 3, + "qna": null, + "displayText": "KB Size Limits" + }, + { + "displayOrder": 0, + "qnaId": 29, + "qna": null, + "displayText": "Other Issues" + } + ] + } + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer.json new file mode 100644 index 000000000..a5fdc06b5 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer.json @@ -0,0 +1,26 @@ +{ + "activeLearningEnabled": true, + "answers": [ + { + "questions": [ + "how do I clean the stove?" + ], + "answer": "BaseCamp: You can use a damp rag to clean around the Power Pack", + "score": 100, + "id": 5, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [ + { + "displayOrder": 0, + "qnaId": 55, + "qna": null, + "displayText": "Where can I buy?" + } + ] + } + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithContext.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithContext.json new file mode 100644 index 000000000..a1c6989ae --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithContext.json @@ -0,0 +1,18 @@ +{ + "answers": [ + { + "questions": [ + "Where can I buy cleaning products?" + ], + "answer": "Any DIY store", + "score": 100, + "id": 55, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [] + } + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithIntent.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithIntent.json new file mode 100644 index 000000000..53d56dc04 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithIntent.json @@ -0,0 +1,26 @@ +{ + "activeLearningEnabled": true, + "answers": [ + { + "questions": [ + "how do I clean the stove?" + ], + "answer": "intent=DeferToRecognizer_xxx", + "score": 100, + "id": 5, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": true, + "prompts": [ + { + "displayOrder": 0, + "qnaId": 55, + "qna": null, + "displayText": "Where can I buy?" + } + ] + } + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithoutContext.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithoutContext.json new file mode 100644 index 000000000..b5cf68875 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswerWithoutContext.json @@ -0,0 +1,32 @@ +{ + "answers": [ + { + "questions": [ + "Where can I buy home appliances?" + ], + "answer": "Any Walmart store", + "score": 68, + "id": 56, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": false, + "prompts": [] + } + }, + { + "questions": [ + "Where can I buy cleaning products?" + ], + "answer": "Any DIY store", + "score": 56, + "id": 55, + "source": "Editorial", + "metadata": [], + "context": { + "isContextOnly": false, + "prompts": [] + } + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer_GivenScoreThresholdQueryOption.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer_GivenScoreThresholdQueryOption.json new file mode 100644 index 000000000..2d9c67652 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer_GivenScoreThresholdQueryOption.json @@ -0,0 +1,19 @@ +{ + "answers": [ + { + "score": 68.54820341616869, + "Id": 22, + "answer": "Why do you ask?", + "source": "Custom Editorial", + "questions": [ + "what happens when you hug a procupine?" + ], + "metadata": [ + { + "name": "animal", + "value": "procupine" + } + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer_WhenNoAnswerFoundInKb.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer_WhenNoAnswerFoundInKb.json new file mode 100644 index 000000000..05e1d3907 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswer_WhenNoAnswerFoundInKb.json @@ -0,0 +1,13 @@ +{ + "answers": [ + { + "questions": [], + "answer": "No good match found in KB.", + "score": 0, + "id": -1, + "source": null, + "metadata": [] + } + ], + "debugInfo": null +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswersWithMetadataBoost.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswersWithMetadataBoost.json new file mode 100644 index 000000000..280467e7b --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsAnswersWithMetadataBoost.json @@ -0,0 +1,19 @@ +{ + "answers": [ + { + "questions": [ + "Who loves me?" + ], + "answer": "Kiki", + "score": 100, + "id": 29, + "source": "Editorial", + "metadata": [ + { + "name": "artist", + "value": "drake" + } + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsNoAnswer.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsNoAnswer.json new file mode 100644 index 000000000..9888a8555 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_ReturnsNoAnswer.json @@ -0,0 +1,4 @@ +{ + "answers": [ + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TestThreshold.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TestThreshold.json new file mode 100644 index 000000000..c8973d9e8 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TestThreshold.json @@ -0,0 +1,3 @@ +{ + "answers": [ ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TopNAnswer.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TopNAnswer.json new file mode 100644 index 000000000..17f598471 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TopNAnswer.json @@ -0,0 +1,65 @@ +{ + "activeLearningEnabled": true, + "answers": [ + { + "questions": [ + "Q1" + ], + "answer": "A1", + "score": 80, + "id": 15, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q2" + ], + "answer": "A2", + "score": 78, + "id": 16, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q3" + ], + "answer": "A3", + "score": 75, + "id": 17, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q4" + ], + "answer": "A4", + "score": 50, + "id": 18, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TopNAnswer_DisableActiveLearning.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TopNAnswer_DisableActiveLearning.json new file mode 100644 index 000000000..e3f714416 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_TopNAnswer_DisableActiveLearning.json @@ -0,0 +1,65 @@ +{ + "activeLearningEnabled": false, + "answers": [ + { + "questions": [ + "Q1" + ], + "answer": "A1", + "score": 80, + "id": 15, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q2" + ], + "answer": "A2", + "score": 78, + "id": 16, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q3" + ], + "answer": "A3", + "score": 75, + "id": 17, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + }, + { + "questions": [ + "Q4" + ], + "answer": "A4", + "score": 50, + "id": 18, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_UsesStrictFilters_ToReturnAnswer.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_UsesStrictFilters_ToReturnAnswer.json new file mode 100644 index 000000000..2dbb1d051 --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/QnaMaker_UsesStrictFilters_ToReturnAnswer.json @@ -0,0 +1,19 @@ +{ + "answers": [ + { + "questions": [ + "how do I clean the stove?" + ], + "answer": "BaseCamp: You can use a damp rag to clean around the Power Pack", + "score": 100, + "id": 5, + "source": "Editorial", + "metadata": [ + { + "name": "topic", + "value": "value" + } + ] + } + ] +} diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/qnamaker.settings.development.westus.json b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/qnamaker.settings.development.westus.json new file mode 100644 index 000000000..b1071df7e --- /dev/null +++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/testdata/qnamaker.settings.development.westus.json @@ -0,0 +1,6 @@ +{ + "qna": { + "sandwichQnA_en_us_qna": "", + "hostname": "" + } +} diff --git a/libraries/bot-applicationinsights/pom.xml b/libraries/bot-applicationinsights/pom.xml new file mode 100644 index 000000000..d9d1e2385 --- /dev/null +++ b/libraries/bot-applicationinsights/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-applicationinsights + jar + + ${project.groupId}:${project.artifactId} + Bot Framework Application Insights + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + org.slf4j + slf4j-api + + + + com.microsoft.azure + applicationinsights-core + 2.6.3 + + + + com.microsoft.bot + bot-dialogs + + + org.mockito + mockito-core + test + + + + com.microsoft.bot + bot-builder + ${project.version} + test-jar + + + + + + build + + true + + + + + + + + diff --git a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java new file mode 100644 index 000000000..2c40f0abc --- /dev/null +++ b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.applicationinsights; + +import com.microsoft.applicationinsights.TelemetryClient; +import com.microsoft.applicationinsights.TelemetryConfiguration; +import com.microsoft.applicationinsights.telemetry.EventTelemetry; +import com.microsoft.applicationinsights.telemetry.ExceptionTelemetry; +import com.microsoft.applicationinsights.telemetry.PageViewTelemetry; +import com.microsoft.applicationinsights.telemetry.RemoteDependencyTelemetry; +import com.microsoft.applicationinsights.telemetry.SeverityLevel; +import com.microsoft.applicationinsights.telemetry.TraceTelemetry; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.Severity; +import org.apache.commons.lang3.StringUtils; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A logging client for bot telemetry. + */ +public class ApplicationInsightsBotTelemetryClient implements BotTelemetryClient { + + private final TelemetryClient telemetryClient; + private final TelemetryConfiguration telemetryConfiguration; + + /** + * Provides access to the Application Insights configuration that is running here. + * Allows developers to adjust the options. + * @return Application insights configuration. + */ + public TelemetryConfiguration getTelemetryConfiguration() { + return telemetryConfiguration; + } + + /** + * Initializes a new instance of the {@link BotTelemetryClient}. + * + * @param instrumentationKey The instrumentation key provided to create + * the {@link ApplicationInsightsBotTelemetryClient}. + */ + public ApplicationInsightsBotTelemetryClient(String instrumentationKey) { + if (StringUtils.isBlank(instrumentationKey)) { + throw new IllegalArgumentException("instrumentationKey should be provided"); + } + this.telemetryConfiguration = TelemetryConfiguration.getActive(); + telemetryConfiguration.setInstrumentationKey(instrumentationKey); + this.telemetryClient = new TelemetryClient(telemetryConfiguration); + } + + /** + * Send information about availability of an application. + * + * @param name Availability test name. + * @param timeStamp The time when the availability was captured. + * @param duration The time taken for the availability test to run. + * @param runLocation Name of the location the availability test was run from. + * @param success True if the availability test ran successfully. + * @param message Error message on availability test run failure. + * @param properties Named string values you can use to classify and search for + * this availability telemetry. + * @param metrics Additional values associated with this availability + * telemetry. + */ + @SuppressWarnings("checkstyle:ParameterNumber") + @Override + public void trackAvailability( + String name, + OffsetDateTime timeStamp, + Duration duration, + String runLocation, + boolean success, + String message, + Map properties, + Map metrics + ) { + com.microsoft.applicationinsights.telemetry.Duration durationTelemetry = + new com.microsoft.applicationinsights.telemetry.Duration(duration.toNanos()); + ConcurrentMap concurrentProperties = new ConcurrentHashMap<>(properties); + ConcurrentMap concurrentMetrics = new ConcurrentHashMap<>(metrics); + AvailabilityTelemetry telemetry = new AvailabilityTelemetry( + name, + durationTelemetry, + runLocation, + message, + success, + concurrentMetrics, + concurrentProperties + ); + telemetry.setTimestamp(new Date(timeStamp.toInstant().toEpochMilli())); + if (properties != null) { + for (Map.Entry pair : properties.entrySet()) { + telemetry.getProperties().put(pair.getKey(), pair.getValue()); + } + } + + if (metrics != null) { + for (Map.Entry pair : metrics.entrySet()) { + telemetry.getMetrics().put(pair.getKey(), pair.getValue()); + } + } + + /** + * This should be telemetryClient.trackAvailability(telemetry). However, it is + * not present in TelemetryClient class + */ + telemetryClient.track(telemetry); + } + + /** + * Send information about an external dependency (outgoing call) in the + * application. + * + * @param dependencyTypeName Name of the command initiated with this dependency + * call. Low cardinality value. Examples are SQL, + * Azure table, and HTTP. + * @param target External dependency target. + * @param dependencyName Name of the command initiated with this dependency + * call. Low cardinality value. Examples are stored + * procedure name and URL path template. + * @param data Command initiated by this dependency call. Examples + * are SQL statement and HTTP URL's with all query + * parameters. + * @param startTime The time when the dependency was called. + * @param duration The time taken by the external dependency to handle + * the call. + * @param resultCode Result code of dependency call execution. + * @param success True if the dependency call was handled + * successfully. + */ + @SuppressWarnings("checkstyle:ParameterNumber") + @Override + public void trackDependency( + String dependencyTypeName, + String target, + String dependencyName, + String data, + OffsetDateTime startTime, + Duration duration, + String resultCode, + boolean success + ) { + com.microsoft.applicationinsights.telemetry.Duration durationTelemetry = + new com.microsoft.applicationinsights.telemetry.Duration(duration.toNanos()); + + RemoteDependencyTelemetry telemetry = + new RemoteDependencyTelemetry(dependencyName, data, durationTelemetry, success); + + telemetry.setType(dependencyTypeName); + telemetry.setTarget(target); + telemetry.setTimestamp(new Date(startTime.toInstant().toEpochMilli())); + telemetry.setResultCode(resultCode); + + telemetryClient.trackDependency(telemetry); + } + + /** + * Logs custom events with extensible named fields. + * + * @param eventName A name for the event. + * @param properties Named string values you can use to search and classify + * events. + * @param metrics Measurements associated with this event. + */ + @Override + public void trackEvent(String eventName, Map properties, Map metrics) { + EventTelemetry telemetry = new EventTelemetry(eventName); + if (properties != null) { + for (Map.Entry pair : properties.entrySet()) { + telemetry.getProperties().put(pair.getKey(), pair.getValue()); + } + } + + if (metrics != null) { + for (Map.Entry pair : metrics.entrySet()) { + telemetry.getMetrics().put(pair.getKey(), pair.getValue()); + } + } + + telemetryClient.trackEvent(telemetry); + } + + /** + * Logs a system exception. + * + * @param exception The exception to log. + * @param properties Named string values you can use to classify and search for + * this exception. + * @param metrics Additional values associated with this exception + */ + @Override + public void trackException(Exception exception, Map properties, Map metrics) { + ExceptionTelemetry telemetry = new ExceptionTelemetry(exception); + if (properties != null) { + for (Map.Entry pair : properties.entrySet()) { + telemetry.getProperties().put(pair.getKey(), pair.getValue()); + } + } + + if (metrics != null) { + for (Map.Entry pair : metrics.entrySet()) { + telemetry.getMetrics().put(pair.getKey(), pair.getValue()); + } + } + + telemetryClient.trackException(telemetry); + } + + /** + * Send a trace message. + * + * @param message Message to display. + * @param severityLevel Trace severity level {@link Severity}. + * @param properties Named string values you can use to search and classify + * events. + */ + @Override + public void trackTrace(String message, Severity severityLevel, Map properties) { + TraceTelemetry telemetry = new TraceTelemetry(message); + telemetry.setSeverityLevel(SeverityLevel.values()[severityLevel.ordinal()]); + + if (properties != null) { + for (Map.Entry pair : properties.entrySet()) { + telemetry.getProperties().put(pair.getKey(), pair.getValue()); + } + } + + telemetryClient.trackTrace(telemetry); + } + + /** + * We implemented this method calling the tracePageView method from + * {@link ApplicationInsightsBotTelemetryClient} as the + * IBotPageViewTelemetryClient has not been implemented. {@inheritDoc} + */ + @Override + public void trackDialogView(String dialogName, Map properties, Map metrics) { + trackPageView(dialogName, properties, metrics); + } + + /** + * Logs a dialog entry / as an Application Insights page view. + * + * @param dialogName The name of the dialog to log the entry / start for. + * @param properties Named string values you can use to search and classify + * events. + * @param metrics Measurements associated with this event. + */ + public void trackPageView(String dialogName, Map properties, Map metrics) { + PageViewTelemetry telemetry = new PageViewTelemetry(dialogName); + + if (properties != null) { + for (Map.Entry pair : properties.entrySet()) { + telemetry.getProperties().put(pair.getKey(), pair.getValue()); + } + } + + if (metrics != null) { + for (Map.Entry pair : metrics.entrySet()) { + telemetry.getMetrics().put(pair.getKey(), pair.getValue()); + } + } + + telemetryClient.trackPageView(telemetry); + } + + /** + * Flushes the in-memory buffer and any metrics being pre-aggregated. + */ + @Override + public void flush() { + telemetryClient.flush(); + } +} diff --git a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/AvailabilityTelemetry.java b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/AvailabilityTelemetry.java new file mode 100644 index 000000000..3197840a3 --- /dev/null +++ b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/AvailabilityTelemetry.java @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.applicationinsights; + +import com.microsoft.applicationinsights.internal.schemav2.AvailabilityData; +import com.microsoft.applicationinsights.internal.util.LocalStringsUtils; +import com.microsoft.applicationinsights.internal.util.Sanitizer; +import com.microsoft.applicationinsights.telemetry.BaseSampleSourceTelemetry; +import com.microsoft.applicationinsights.telemetry.Duration; +import java.util.Date; +import java.util.concurrent.ConcurrentMap; + +/** + * We took this class from + * https://github.com/microsoft/ApplicationInsights-Java/issues/1099 as this is + * not already migrated in ApplicationInsights-Java library. + */ +public final class AvailabilityTelemetry extends BaseSampleSourceTelemetry { + private Double samplingPercentage; + private final AvailabilityData data; + + public static final String ENVELOPE_NAME = "Availability"; + + public static final String BASE_TYPE = "AvailabilityData"; + + /** + * Initializes a new instance of the AvailabilityTelemetry class. + */ + public AvailabilityTelemetry() { + this.data = new AvailabilityData(); + initialize(this.data.getProperties()); + setId(LocalStringsUtils.generateRandomIntegerId()); + + // Setting mandatory fields. + setTimestamp(new Date()); + setSuccess(true); + } + + /** + * Initializes a new instance of the AvailabilityTelemetry class with the given + * name, time stamp, duration, HTTP response code and success property values. + * + * @param name A user-friendly name for the request. + * @param duration The time of the request. + * @param runLocation The duration, in milliseconds, of the request processing. + * @param message The HTTP response code. + * @param success 'true' if the request was a success, 'false' otherwise. + * @param measurements The measurements. + * @param properties The corresponding properties. + */ + public AvailabilityTelemetry( + String name, + Duration duration, + String runLocation, + String message, + boolean success, + ConcurrentMap measurements, + ConcurrentMap properties + ) { + + this.data = new AvailabilityData(); + + this.data.setProperties(properties); + this.data.setMeasurements(measurements); + this.data.setMessage(message); + + initialize(this.data.getProperties()); + + setId(LocalStringsUtils.generateRandomIntegerId()); + + setTimestamp(new Date()); + + setName(name); + setRunLocation(runLocation); + setDuration(duration); + setSuccess(success); + } + + /** + * Gets the ver value from the data object. + * + * @return The ver value. + */ + @Override + public int getVer() { + return getData().getVer(); + } + + /** + * Gets a map of application-defined request metrics. + * + * @return The map of metrics + */ + public ConcurrentMap getMetrics() { + return data.getMeasurements(); + } + + /** + * Sets the StartTime. Uses the default behavior and sets the property on the + * 'data' start time. + * + * @param timestamp The timestamp as Date. + */ + @Override + public void setTimestamp(Date timestamp) { + if (timestamp == null) { + timestamp = new Date(); + } + + super.setTimestamp(timestamp); + } + + /** + * Gets or human-readable name of the requested page. + * + * @return A human-readable name. + */ + public String getName() { + return data.getName(); + } + + /** + * Sets or human-readable name of the requested page. + * + * @param name A human-readable name. + */ + public void setName(String name) { + data.setName(name); + } + + /** + * Gets or human-readable name of the run location. + * + * @return A human-readable name. + */ + public String getRunLocation() { + return data.getRunLocation(); + } + + /** + * Sets or human-readable name of the run location. + * + * @param runLocation A human-readable name + */ + public void setRunLocation(String runLocation) { + data.setRunLocation(runLocation); + } + + /** + * Gets the unique identifier of the request. + * + * @return Unique identifier. + */ + public String getId() { + return data.getId(); + } + + /** + * Sets the unique identifier of the request. + * + * @param id Unique identifier. + */ + public void setId(String id) { + data.setId(id); + } + + /** + * Gets a value indicating whether application handled the request successfully. + * + * @return Success indication. + */ + public boolean isSuccess() { + return data.getSuccess(); + } + + /** + * Sets a value indicating whether application handled the request successfully. + * + * @param success Success indication. + */ + public void setSuccess(boolean success) { + data.setSuccess(success); + } + + /** + * Gets the amount of time it took the application to handle the request. + * + * @return Amount of time in milliseconds. + */ + public Duration getDuration() { + return data.getDuration(); + } + + /** + * Sets the amount of time it took the application to handle the request. + * + * @param duration Amount of time in captured in a + * {@link com.microsoft.applicationinsights.telemetry.Duration}. + */ + public void setDuration(Duration duration) { + data.setDuration(duration); + } + + @Override + public Double getSamplingPercentage() { + return samplingPercentage; + } + + @Override + public void setSamplingPercentage(Double samplingPercentage) { + this.samplingPercentage = samplingPercentage; + } + + @Override + @Deprecated + protected void additionalSanitize() { + data.setName(Sanitizer.sanitizeName(data.getName())); + data.setId(Sanitizer.sanitizeName(data.getId())); + Sanitizer.sanitizeMeasurements(getMetrics()); + } + + @Override + protected AvailabilityData getData() { + return data; + } + + @Override + public String getEnvelopName() { + return ENVELOPE_NAME; + } + + @Override + public String getBaseTypeName() { + return BASE_TYPE; + } +} diff --git a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/core/TelemetryInitializerMiddleware.java b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/core/TelemetryInitializerMiddleware.java new file mode 100644 index 000000000..8bf4377be --- /dev/null +++ b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/core/TelemetryInitializerMiddleware.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.applicationinsights.core; + +import com.microsoft.applicationinsights.core.dependencies.http.client.protocol.HttpClientContext; +import com.microsoft.applicationinsights.core.dependencies.http.protocol.HttpContext; +import com.microsoft.bot.builder.BotAssert; +import com.microsoft.bot.builder.Middleware; +import com.microsoft.bot.builder.NextDelegate; +import com.microsoft.bot.builder.TelemetryLoggerMiddleware; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.schema.Activity; + +import java.util.concurrent.CompletableFuture; + +/** + * Middleware for storing incoming activity on the HttpContext. + */ +public class TelemetryInitializerMiddleware implements Middleware { + + private HttpContext httpContext; + private final String botActivityKey = "BotBuilderActivity"; + private final TelemetryLoggerMiddleware telemetryLoggerMiddleware; + private final Boolean logActivityTelemetry; + + /** + * Initializes a new instance of the {@link TelemetryInitializerMiddleware}. + * + * @param withTelemetryLoggerMiddleware The TelemetryLoggerMiddleware to use. + * @param withLogActivityTelemetry Boolean determining if you want to log + * telemetry activity + */ + public TelemetryInitializerMiddleware( + TelemetryLoggerMiddleware withTelemetryLoggerMiddleware, + Boolean withLogActivityTelemetry + ) { + telemetryLoggerMiddleware = withTelemetryLoggerMiddleware; + if (withLogActivityTelemetry == null) { + withLogActivityTelemetry = true; + } + logActivityTelemetry = withLogActivityTelemetry; + } + + /** + * Stores the incoming activity as JSON in the items collection on the + * HttpContext. + * + * @param context The incoming TurnContext + * @param next Delegate to run next on + * @return Returns a CompletableFuture with Void value + */ + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + BotAssert.contextNotNull(context); + + if (context.getActivity() != null) { + Activity activity = context.getActivity(); + + if (this.httpContext == null) { + this.httpContext = HttpClientContext.create(); + } + + Object item = httpContext.getAttribute(botActivityKey); + + if (item != null) { + httpContext.removeAttribute(botActivityKey); + } + + httpContext.setAttribute(botActivityKey, activity); + } + + if (logActivityTelemetry) { + return telemetryLoggerMiddleware.onTurn(context, next); + } else { + return next.next(); + } + } +} diff --git a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/core/package-info.java b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/core/package-info.java new file mode 100644 index 000000000..be4da7780 --- /dev/null +++ b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/core/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.applicationinsights.core. + */ +package com.microsoft.bot.applicationinsights.core; diff --git a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/package-info.java b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/package-info.java new file mode 100644 index 000000000..549072bb9 --- /dev/null +++ b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.applicationinsights. + */ +@Deprecated +package com.microsoft.bot.applicationinsights; diff --git a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java new file mode 100644 index 000000000..3d0a8b014 --- /dev/null +++ b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.applicationinsights; + +import com.microsoft.applicationinsights.channel.TelemetryChannel; +import com.microsoft.applicationinsights.telemetry.EventTelemetry; +import com.microsoft.applicationinsights.telemetry.RemoteDependencyTelemetry; +import com.microsoft.applicationinsights.telemetry.PageViewTelemetry; +import com.microsoft.applicationinsights.telemetry.ExceptionTelemetry; +import com.microsoft.applicationinsights.telemetry.TraceTelemetry; +import com.microsoft.applicationinsights.telemetry.SeverityLevel; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.Severity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; + +public class BotTelemetryClientTests { + + private ApplicationInsightsBotTelemetryClient botTelemetryClient; + private TelemetryChannel mockTelemetryChannel; + + @Before + public void initialize() { + botTelemetryClient = new ApplicationInsightsBotTelemetryClient("fakeKey"); + mockTelemetryChannel = Mockito.mock(TelemetryChannel.class); + botTelemetryClient.getTelemetryConfiguration().setChannel(mockTelemetryChannel); + } + + @Test + public void nullTelemetryClientThrows() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + new ApplicationInsightsBotTelemetryClient(null); + }); + } + + @Test + public void emptyTelemetryClientThrows() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + new ApplicationInsightsBotTelemetryClient(""); + }); + } + + @Test + public void nonNullTelemetryClientSucceeds() { + BotTelemetryClient botTelemetryClient = new ApplicationInsightsBotTelemetryClient("fakeKey"); + } + + @Test + public void overrideTest() { + MyBotTelemetryClient botTelemetryClient = new MyBotTelemetryClient("fakeKey"); + } + + @Test + public void trackAvailabilityTest() { + Map properties = new HashMap<>(); + Map metrics = new HashMap<>(); + properties.put("hello", "value"); + metrics.put("metric", 0.6); + + botTelemetryClient.trackAvailability( + "test", + OffsetDateTime.now(), + Duration.ofNanos(1000), + "run location", + true, + "message", + properties, + metrics); + + Mockito.verify(mockTelemetryChannel, invocations -> { + AvailabilityTelemetry availabilityTelemetry = invocations.getAllInvocations().get(0).getArgument(0); + Assert.assertEquals("test", availabilityTelemetry.getName()); + Assert.assertEquals("message", availabilityTelemetry.getData().getMessage()); + Assert.assertEquals("value", availabilityTelemetry.getProperties().get("hello")); + Assert.assertEquals(0, Double.compare(0.6, availabilityTelemetry.getMetrics().get("metric"))); + }).send(Mockito.any(AvailabilityTelemetry.class)); + + } + + @Test + public void trackEventTest() { + Map properties = new HashMap<>(); + properties.put("hello", "value"); + Map metrics = new HashMap<>(); + metrics.put("metric", 0.6); + + botTelemetryClient.trackEvent("test", properties, metrics); + + Mockito.verify(mockTelemetryChannel, invocations -> { + EventTelemetry eventTelemetry = invocations.getAllInvocations().get(0).getArgument(0); + + Assert.assertEquals("test", eventTelemetry.getName()); + Assert.assertEquals("value", eventTelemetry.getProperties().get("hello")); + Assert.assertEquals(0, Double.compare(0.6, eventTelemetry.getMetrics().get("metric"))); + }).send(Mockito.any(EventTelemetry.class)); + } + + @Test + public void trackDependencyTest() { + botTelemetryClient.trackDependency( + "test", + "target", + "dependencyname", + "data", + OffsetDateTime.now(), + Duration.ofNanos(1000), + "result", + false); + + Mockito.verify(mockTelemetryChannel, invocations -> { + RemoteDependencyTelemetry remoteDependencyTelemetry = invocations.getAllInvocations().get(0).getArgument(0); + + Assert.assertEquals("test", remoteDependencyTelemetry.getType()); + Assert.assertEquals("target", remoteDependencyTelemetry.getTarget()); + Assert.assertEquals("dependencyname", remoteDependencyTelemetry.getName()); + Assert.assertEquals("result", remoteDependencyTelemetry.getResultCode()); + Assert.assertFalse(remoteDependencyTelemetry.getSuccess()); + }).send(Mockito.any(RemoteDependencyTelemetry.class)); + } + + @Test + public void trackExceptionTest() { + Exception expectedException = new Exception("test-exception"); + Map properties = new HashMap<>(); + properties.put("foo", "bar"); + Map metrics = new HashMap<>(); + metrics.put("metric", 0.6); + + botTelemetryClient.trackException(expectedException, properties, metrics); + + Mockito.verify(mockTelemetryChannel, invocations -> { + ExceptionTelemetry exceptionTelemetry = invocations.getAllInvocations().get(0).getArgument(0); + + Assert.assertEquals(expectedException, exceptionTelemetry.getException()); + Assert.assertEquals("bar", exceptionTelemetry.getProperties().get("foo")); + Assert.assertEquals(0, Double.compare(0.6, exceptionTelemetry.getMetrics().get("metric"))); + }).send(Mockito.any(ExceptionTelemetry.class)); + } + + @Test + public void trackTraceTest() { + Map properties = new HashMap<>(); + properties.put("foo", "bar"); + + botTelemetryClient.trackTrace("hello", Severity.CRITICAL, properties); + + Mockito.verify(mockTelemetryChannel, invocations -> { + TraceTelemetry traceTelemetry = invocations.getAllInvocations().get(0).getArgument(0); + + Assert.assertEquals("hello", traceTelemetry.getMessage()); + Assert.assertEquals(SeverityLevel.Critical, traceTelemetry.getSeverityLevel()); + Assert.assertEquals("bar", traceTelemetry.getProperties().get("foo")); + }).send(Mockito.any(TraceTelemetry.class)); + } + + @Test + public void trackPageViewTest() { + Map properties = new HashMap<>(); + properties.put("hello", "value"); + Map metrics = new HashMap<>(); + metrics.put("metric", 0.6); + + botTelemetryClient.trackDialogView("test", properties, metrics); + + Mockito.verify(mockTelemetryChannel, invocations -> { + PageViewTelemetry pageViewTelemetry = invocations.getAllInvocations().get(0).getArgument(0); + + Assert.assertEquals("test", pageViewTelemetry.getName()); + Assert.assertEquals("value", pageViewTelemetry.getProperties().get("hello")); + Assert.assertEquals(0, Double.compare(0.6, pageViewTelemetry.getMetrics().get("metric"))); + }).send(Mockito.any(PageViewTelemetry.class)); + } + + @Test + public void flushTest() { + botTelemetryClient.flush(); + + Mockito.verify(mockTelemetryChannel, invocations -> { + Assert.assertEquals(1, invocations.getAllInvocations().size()); + }).send(Mockito.any()); + } +} diff --git a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java new file mode 100644 index 000000000..acc48e9c8 --- /dev/null +++ b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.applicationinsights; + +import com.microsoft.bot.builder.Severity; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Map; + +public class MyBotTelemetryClient extends ApplicationInsightsBotTelemetryClient { + public MyBotTelemetryClient(String instrumentationKey) { + super(instrumentationKey); + } + + @Override + public void trackDependency( + String dependencyTypeName, + String target, + String dependencyName, + String data, + OffsetDateTime startTime, + Duration duration, + String resultCode, + boolean success) + { + super.trackDependency(dependencyName, target, dependencyName, data, startTime, duration, resultCode, success); + } + + @Override + public void trackAvailability( + String name, + OffsetDateTime timeStamp, + Duration duration, + String runLocation, + boolean success, + String message, + Map properties, + Map metrics) + { + super.trackAvailability(name, timeStamp, duration, runLocation, success, message, properties, metrics); + } + + @Override + public void trackEvent( + String eventName, + Map properties, + Map metrics) + { + super.trackEvent(eventName, properties, metrics); + } + + @Override + public void trackException( + Exception exception, + Map properties, + Map metrics) + { + super.trackException(exception, properties, metrics); + } + + @Override + public void trackTrace( + String message, + Severity severityLevel, + Map properties) + { + super.trackTrace(message, severityLevel, properties); + } + + @Override + public void trackPageView( + String name, + Map properties, + Map metrics) + { + super.trackPageView(name, properties, metrics); + } + + @Override + public void flush() + { + super.flush(); + } +} diff --git a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/TelemetryInitializerTests.java b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/TelemetryInitializerTests.java new file mode 100644 index 000000000..3eb59f70a --- /dev/null +++ b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/TelemetryInitializerTests.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.applicationinsights; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; + +import com.microsoft.bot.applicationinsights.core.TelemetryInitializerMiddleware; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.TelemetryLoggerMiddleware; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TelemetryInitializerTests { + + @Captor + ArgumentCaptor eventNameCaptor; + + @Captor + ArgumentCaptor> propertiesCaptor; + + @Test + public void telemetryInitializerMiddlewareLogActivitiesEnabled() { + + // Arrange + BotTelemetryClient mockTelemetryClient = Mockito.mock(BotTelemetryClient.class); + TelemetryLoggerMiddleware telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(mockTelemetryClient, false); + + TestAdapter testAdapter = new TestAdapter() + .use(new TelemetryInitializerMiddleware(telemetryLoggerMiddleware, true)); + + // Act + // Default case logging Send/Receive Activities + new TestFlow(testAdapter, turnContext -> { + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + + turnContext.sendActivity(typingActivity).join(); + try { + TimeUnit.MILLISECONDS.sleep(500); + } catch (InterruptedException e) { + // Empty error + } + turnContext.sendActivity(String.format("echo:%s", turnContext.getActivity().getText())).join(); + return CompletableFuture.completedFuture(null); + }) + .send("foo") + .assertReply(activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.TYPING)); + }) + .assertReply("echo:foo") + .send("bar") + .assertReply(activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.TYPING)); + }) + .assertReply("echo:bar") + .startTest().join(); + + // Verify + verify(mockTelemetryClient, times(6)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + + List eventNames = eventNameCaptor.getAllValues(); + Assert.assertEquals(6, eventNames.size()); + + } + + @Test + public void telemetryInitializerMiddlewareNotLogActivitiesDisabled() { + + // Arrange + BotTelemetryClient mockTelemetryClient = Mockito.mock(BotTelemetryClient.class); + TelemetryLoggerMiddleware telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(mockTelemetryClient, false); + + TestAdapter testAdapter = new TestAdapter() + .use(new TelemetryInitializerMiddleware(telemetryLoggerMiddleware, false)); + + // Act + // Default case logging Send/Receive Activities + new TestFlow(testAdapter, (turnContext) -> { + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + + turnContext.sendActivity(typingActivity).join(); + try { + TimeUnit.MILLISECONDS.sleep(500); + } catch (InterruptedException e) { + // Empty error + } + turnContext.sendActivity(String.format("echo:%s", turnContext.getActivity().getText())).join(); + return CompletableFuture.completedFuture(null); + }) + .send("foo") + .assertReply(activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.TYPING)); + }) + .assertReply("echo:foo") + .send("bar") + .assertReply(activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.TYPING)); + }) + .assertReply("echo:bar") + .startTest().join(); + + // Verify + verify(mockTelemetryClient, times(0)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + Assert.assertEquals(0, eventNames.size()); + } + + @Test + public void telemetryInitializerMiddlewareWithUndefinedContext() { + // Arrange + BotTelemetryClient mockTelemetryClient = Mockito.mock(BotTelemetryClient.class); + TelemetryLoggerMiddleware telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(mockTelemetryClient, false); + TelemetryInitializerMiddleware telemetryInitializerMiddleware = new TelemetryInitializerMiddleware(telemetryLoggerMiddleware, true); + // Assert + Assert.assertThrows(IllegalArgumentException.class, () -> { + // Act + telemetryInitializerMiddleware.onTurn(null, () -> null); + }); + } +} diff --git a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/TelemetryWaterfallTests.java b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/TelemetryWaterfallTests.java new file mode 100644 index 000000000..70825410b --- /dev/null +++ b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/TelemetryWaterfallTests.java @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.applicationinsights; + +import com.microsoft.bot.builder.AutoSaveStateMiddleware; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MemoryStorage; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogInstance; +import com.microsoft.bot.dialogs.DialogReason; +import com.microsoft.bot.dialogs.DialogSet; +import com.microsoft.bot.dialogs.DialogState; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class TelemetryWaterfallTests { + + @Test + public void waterfall() { + ConversationState convoState = new ConversationState(new MemoryStorage()); + TestAdapter adapter = new TestAdapter(TestAdapter.createConversationReference("Waterfall", "User1", "Bot")) + .use(new AutoSaveStateMiddleware(convoState)); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + StatePropertyAccessor dialogState = convoState.createProperty("dialogState"); + DialogSet dialogs = new DialogSet(dialogState); + + dialogs.add(new WaterfallDialog("test", newWaterfall())); + dialogs.setTelemetryClient(telemetryClient); + + new TestFlow(adapter, turnContext -> { + DialogContext dc = dialogs.createContext(turnContext).join(); + dc.continueDialog().join(); + if (!turnContext.getResponded()) { + dc.beginDialog("test", null).join(); + } + return CompletableFuture.completedFuture(null); + }) + .send("hello") + .assertReply("step1") + .send("hello") + .assertReply("step2") + .send("hello") + .assertReply("step3") + .startTest() + .join(); + + // C#'s trackEvent method of BotTelemetryClient has nullable parameters, + // therefore it always calls the same method. + // On the other hand, Java's BotTelemetryClient overloads the trackEvent method, + // so instead of calling the same method, it calls a method with less parameters. + // In this particular test, WaterfallDialog's beginDialog calls the method with only two parameters + Mockito.verify(telemetryClient, Mockito.times(4)).trackEvent( + Mockito.anyString(), + Mockito.anyMap() + ); + System.out.printf("Complete"); + } + + @Test + public void waterfallWithCallback() { + ConversationState convoState = new ConversationState(new MemoryStorage()); + TestAdapter adapter = new TestAdapter(TestAdapter.createConversationReference("WaterfallWithCallback", "User1", "Bot")) + .use(new AutoSaveStateMiddleware(convoState)); + + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + StatePropertyAccessor dialogState = convoState.createProperty("dialogState"); + DialogSet dialogs = new DialogSet(dialogState); + WaterfallDialog waterfallDialog = new WaterfallDialog("test", newWaterfall()); + + dialogs.add(waterfallDialog); + dialogs.setTelemetryClient(telemetryClient); + + new TestFlow(adapter, turnContext -> { + DialogContext dc = dialogs.createContext(turnContext).join(); + dc.continueDialog().join(); + if (!turnContext.getResponded()) { + dc.beginDialog("test", null).join(); + } + return CompletableFuture.completedFuture(null); + }) + .send("hello") + .assertReply("step1") + .send("hello") + .assertReply("step2") + .send("hello") + .assertReply("step3") + .startTest() + .join(); + + // C#'s trackEvent method of BotTelemetryClient has nullable parameters, + // therefore it always calls the same method. + // On the other hand, Java's BotTelemetryClient overloads the trackEvent method, + // so instead of calling the same method, it calls a method with less parameters. + // In this particular test, WaterfallDialog's beginDialog calls the method with only two parameters + Mockito.verify(telemetryClient, Mockito.times(4)).trackEvent( + Mockito.anyString(), + Mockito.anyMap() + ); + } + + @Test + public void waterfallWithActionsNull() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + WaterfallDialog waterfall = new WaterfallDialog("test", null); + waterfall.setTelemetryClient(telemetryClient); + waterfall.addStep(null); + }); + } + + @Test + public void ensureEndDialogCalled() { + ConversationState convoState = new ConversationState(new MemoryStorage()); + TestAdapter adapter = new TestAdapter(TestAdapter.createConversationReference("EnsureEndDialogCalled", "User1", "Bot")) + .use(new AutoSaveStateMiddleware(convoState)); + + StatePropertyAccessor dialogState = convoState.createProperty("dialogState"); + DialogSet dialogs = new DialogSet(dialogState); + HashMap> saved_properties = new HashMap<>(); + final int[] counter = {0}; + + // Set up the client to save all logged property names and associated properties (in "saved_properties"). + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + Mockito.doAnswer(invocation -> { + String eventName = invocation.getArgument(0); + Map properties = invocation.getArgument(1); + + StringBuilder sb = new StringBuilder(eventName).append("_").append(counter[0]++); + saved_properties.put(sb.toString(), properties); + + return null; + }).when(telemetryClient).trackEvent(Mockito.anyString(), Mockito.anyMap()); + + MyWaterfallDialog waterfallDialog = new MyWaterfallDialog("test", newWaterfall()); + + dialogs.add(waterfallDialog); + dialogs.setTelemetryClient(telemetryClient); + + new TestFlow(adapter, turnContext -> { + DialogContext dc = dialogs.createContext(turnContext).join(); + dc.continueDialog().join(); + if (!turnContext.getResponded()) { + dc.beginDialog("test", null).join(); + } + return CompletableFuture.completedFuture(null); + }) + .send("hello") + .assertReply("step1") + .send("hello") + .assertReply("step2") + .send("hello") + .assertReply("step3") + .send("hello") + .assertReply("step1") + .startTest() + .join(); + + Mockito.verify(telemetryClient, Mockito.times(7)).trackEvent( + Mockito.anyString(), + Mockito.anyMap() + ); + + // Verify: + // Event name is "WaterfallComplete" + // Event occurs on the 4th event logged + // Event contains DialogId + // Event DialogId is set correctly. + Assert.assertTrue(saved_properties.get("WaterfallComplete_4").containsKey("DialogId")); + Assert.assertEquals("test", saved_properties.get("WaterfallComplete_4").get("DialogId")); + Assert.assertTrue(saved_properties.get("WaterfallComplete_4").containsKey("InstanceId")); + Assert.assertTrue(saved_properties.get("WaterfallStep_1").containsKey("InstanceId")); + + // Verify naming on lambda's is "StepXofY" + Assert.assertTrue(saved_properties.get("WaterfallStep_1").containsKey("StepName")); + Assert.assertEquals("Step1of3", saved_properties.get("WaterfallStep_1").get("StepName")); + Assert.assertTrue(saved_properties.get("WaterfallStep_1").containsKey("InstanceId")); + Assert.assertTrue(waterfallDialog.getEndDialogCalled()); + } + + @Test + public void ensureCancelDialogCalled() { + ConversationState convoState = new ConversationState(new MemoryStorage()); + TestAdapter adapter = new TestAdapter(TestAdapter.createConversationReference("EnsureCancelDialogCalled", "User1", "Bot")) + .use(new AutoSaveStateMiddleware(convoState)); + + StatePropertyAccessor dialogState = convoState.createProperty("dialogState"); + DialogSet dialogs = new DialogSet(dialogState); + HashMap> saved_properties = new HashMap<>(); + final int[] counter = {0}; + + // Set up the client to save all logged property names and associated properties (in "saved_properties"). + BotTelemetryClient telemetryClient = Mockito.mock(BotTelemetryClient.class); + Mockito.doAnswer(invocation -> { + String eventName = invocation.getArgument(0); + Map properties = invocation.getArgument(1); + + StringBuilder sb = new StringBuilder(eventName).append("_").append(counter[0]++); + saved_properties.put(sb.toString(), properties); + + return null; + }).when(telemetryClient).trackEvent(Mockito.anyString(), Mockito.anyMap()); + + List steps = new ArrayList<>(); + steps.add(step -> { + step.getContext().sendActivity("step1").join(); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + steps.add(step -> { + step.getContext().sendActivity("step2").join(); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + steps.add(step -> { + step.cancelAllDialogs().join(); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + + MyWaterfallDialog waterfallDialog = new MyWaterfallDialog("test", steps); + + dialogs.add(waterfallDialog); + dialogs.setTelemetryClient(telemetryClient); + + new TestFlow(adapter, turnContext -> { + DialogContext dc = dialogs.createContext(turnContext).join(); + dc.continueDialog().join(); + if (!turnContext.getResponded()) { + dc.beginDialog("test", null).join(); + } + return CompletableFuture.completedFuture(null); + }) + .send("hello") + .assertReply("step1") + .send("hello") + .assertReply("step2") + .send("hello") + .assertReply("step1") + .startTest() + .join(); + + Mockito.verify(telemetryClient, Mockito.times(7)).trackEvent( + Mockito.anyString(), + Mockito.anyMap() + ); + + // Verify: + // Event name is "WaterfallCancel" + // Event occurs on the 4th event logged + // Event contains DialogId + // Event DialogId is set correctly. + Assert.assertTrue(saved_properties.get("WaterfallStart_0").containsKey("DialogId")); + Assert.assertTrue(saved_properties.get("WaterfallStart_0").containsKey("InstanceId")); + Assert.assertTrue(saved_properties.get("WaterfallCancel_4").containsKey("DialogId")); + Assert.assertEquals("test", saved_properties.get("WaterfallCancel_4").get("DialogId")); + Assert.assertTrue(saved_properties.get("WaterfallCancel_4").containsKey("StepName")); + Assert.assertTrue(saved_properties.get("WaterfallCancel_4").containsKey("InstanceId")); + + // Event contains "StepName" + // Event naming on lambda's is "StepXofY" + Assert.assertEquals("Step3of3", saved_properties.get("WaterfallCancel_4").get("StepName")); + Assert.assertTrue(waterfallDialog.getCancelDialogCalled()); + Assert.assertFalse(waterfallDialog.getEndDialogCalled()); + } + + private static List newWaterfall() { + List waterfall = new ArrayList<>(); + + waterfall.add(step -> { + step.getContext().sendActivity("step1").join(); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + + waterfall.add(step -> { + step.getContext().sendActivity("step2").join(); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + + waterfall.add(step -> { + step.getContext().sendActivity("step3").join(); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + + return waterfall; + } + + private class MyWaterfallDialog extends WaterfallDialog { + private Boolean endDialogCalled = false; + private Boolean cancelDialogCalled = false; + + public MyWaterfallDialog(String id, List actions) { + super(id, actions); + } + + @Override + public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + if (reason == DialogReason.END_CALLED) { + endDialogCalled = true; + } else if (reason == DialogReason.CANCEL_CALLED) { + cancelDialogCalled = true; + } + + return super.endDialog(turnContext, instance, reason); + } + + public Boolean getEndDialogCalled() { + return endDialogCalled; + } + + public Boolean getCancelDialogCalled() { + return cancelDialogCalled; + } + } +} diff --git a/libraries/bot-azure/pom.xml b/libraries/bot-azure/pom.xml new file mode 100644 index 000000000..0ada29d2b --- /dev/null +++ b/libraries/bot-azure/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-azure + jar + + ${project.groupId}:${project.artifactId} + Bot Framework Azure + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + org.slf4j + slf4j-api + + + + com.microsoft.azure + azure-documentdb + 2.6.4 + + + + com.microsoft.bot + bot-builder + + + com.microsoft.bot + bot-integration-core + + + com.microsoft.bot + bot-dialogs + + + + com.azure + azure-storage-queue + 12.15.0 + + + + com.microsoft.bot + bot-builder + ${project.version} + test-jar + test + + + + com.azure + azure-storage-blob + 12.20.0 + + + + + + build + + true + + + + + + + + + diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbKeyEscape.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbKeyEscape.java new file mode 100644 index 000000000..d952ed901 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbKeyEscape.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Helper class to escape CosmosDB keys. + */ +public final class CosmosDbKeyEscape { + + private CosmosDbKeyEscape() { + // not called + } + + private static final Integer ESCAPE_LENGTH = 3; + + /** + * Older libraries had a max key length of 255. The limit is now 1023. In this + * library, 255 remains the default for backwards compat. To override this + * behavior, and use the longer limit set + * CosmosDbPartitionedStorageOptions.CompatibilityMode to false. + * https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits. + */ + public static final Integer MAX_KEY_LENGTH = 255; + + /** + * The list of illegal characters for Cosmos DB Keys comes from this list on the + * CosmostDB docs: + * https://docs.microsoft.com/dotnet/api/microsoft.azure.documents.resource.id?view=azure-dotnet#remarks + * + * Note: We are also escaping the "*" character, as that what we're using as our + * escape character. + * + * Note: The Java version escapes more than .NET since otherwise it errors out. + * The additional characters are quote, single quote, semi-colon. + */ + private static final char[] ILLEGAL_KEYS = new char[] {'\\', '?', '/', '#', '*', ';', '\"', '\''}; + + /** + * We are escaping illegal characters using a "*{AsciiCodeInHex}" pattern. This + * means a key of "?test?" would be escaped as "*3ftest*3f". + */ + private static final Map ILLEGAL_KEY_CHARACTER_REPLACEMENT_MAP = Arrays + .stream(ArrayUtils.toObject(ILLEGAL_KEYS)) + .collect(Collectors.toMap(c -> c, c -> "*" + String.format("%02x", (int) c))); + + /** + * Converts the key into a DocumentID that can be used safely with Cosmos DB. + * + * @param key The key to escape. + * @return An escaped key that can be used safely with CosmosDB. + * + * @see #ILLEGAL_KEYS + */ + public static String escapeKey(String key) { + return escapeKey(key, new String(), true); + } + + /** + * Converts the key into a DocumentID that can be used safely with Cosmos DB. + * + * @param key The key to escape. + * @param suffix The string to add at the end of all row keys. + * @param compatibilityMode True if running in compatability mode and keys + * should be truncated in order to support previous + * CosmosDb max key length of 255. This behavior can be + * overridden by setting + * {@link CosmosDbPartitionedStorage.compatibilityMode} + * to false. * + * @return An escaped key that can be used safely with CosmosDB. + */ + public static String escapeKey(String key, String suffix, Boolean compatibilityMode) { + if (StringUtils.isBlank(key)) { + throw new IllegalArgumentException("key"); + } + + suffix = suffix == null ? new String() : suffix; + + Integer firstIllegalCharIndex = StringUtils.indexOfAny(key, new String(ILLEGAL_KEYS)); + + // If there are no illegal characters, and the key is within length costraints, + // return immediately and avoid any further processing/allocations + if (firstIllegalCharIndex == -1) { + return truncateKeyIfNeeded(key.concat(suffix), compatibilityMode); + } + + // Allocate a builder that assumes that all remaining characters might be + // replaced + // to avoid any extra allocations + StringBuilder sanitizedKeyBuilder = + new StringBuilder(key.length() + ((key.length() - firstIllegalCharIndex) * ESCAPE_LENGTH)); + + // Add all good characters up to the first bad character to the builder first + for (Integer index = 0; index < firstIllegalCharIndex; index++) { + sanitizedKeyBuilder.append(key.charAt(index)); + } + + Map illegalCharacterReplacementMap = ILLEGAL_KEY_CHARACTER_REPLACEMENT_MAP; + + // Now walk the remaining characters, starting at the first known bad character, + // replacing any bad ones with + // their designated replacement value from the + for (Integer index = firstIllegalCharIndex; index < key.length(); index++) { + Character ch = key.charAt(index); + + // Check if this next character is considered illegal and, if so, append its + // replacement; + // otherwise just append the good character as is + if (illegalCharacterReplacementMap.containsKey(ch)) { + sanitizedKeyBuilder.append(illegalCharacterReplacementMap.get(ch)); + } else { + sanitizedKeyBuilder.append(ch); + } + } + + if (StringUtils.isNotBlank(key)) { + sanitizedKeyBuilder.append(suffix); + } + + return truncateKeyIfNeeded(sanitizedKeyBuilder.toString(), compatibilityMode); + } + + private static String truncateKeyIfNeeded(String key, Boolean truncateKeysForCompatibility) { + if (!truncateKeysForCompatibility) { + return key; + } + + if (key.length() > MAX_KEY_LENGTH) { + String hash = String.format("%x", key.hashCode()); + key = key.substring(0, MAX_KEY_LENGTH - hash.length()) + hash; + } + + return key; + } +} diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbPartitionedStorage.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbPartitionedStorage.java new file mode 100644 index 000000000..037b6f4e2 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbPartitionedStorage.java @@ -0,0 +1,506 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.azure.documentdb.AccessCondition; +import com.microsoft.azure.documentdb.AccessConditionType; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClient; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.PartitionKey; +import com.microsoft.azure.documentdb.PartitionKeyDefinition; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.StoreItem; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * Implements an CosmosDB based storage provider using partitioning for a bot. + */ +public class CosmosDbPartitionedStorage implements Storage { + private Logger logger = LoggerFactory.getLogger(CosmosDbPartitionedStorage.class); + private CosmosDbPartitionedStorageOptions cosmosDbStorageOptions; + private ObjectMapper objectMapper; + private final Object cacheSync = new Object(); + private DocumentClient client; + private Database databaseCache; + private DocumentCollection collectionCache; + + /** + * Initializes a new instance of the CosmosDbPartitionedStorage class. using the + * provided CosmosDB credentials, database ID, and container ID. + * + * @param withCosmosDbStorageOptions Cosmos DB partitioned storage configuration + * options. + */ + public CosmosDbPartitionedStorage(CosmosDbPartitionedStorageOptions withCosmosDbStorageOptions) { + if (withCosmosDbStorageOptions == null) { + throw new IllegalArgumentException("CosmosDbPartitionStorageOptions is required."); + } + + if (withCosmosDbStorageOptions.getCosmosDbEndpoint() == null) { + throw new IllegalArgumentException("Service EndPoint for CosmosDB is required: cosmosDbEndpoint"); + } + + if (StringUtils.isBlank(withCosmosDbStorageOptions.getAuthKey())) { + throw new IllegalArgumentException("AuthKey for CosmosDB is required: authKey"); + } + + if (StringUtils.isBlank(withCosmosDbStorageOptions.getDatabaseId())) { + throw new IllegalArgumentException("DatabaseId is required: databaseId"); + } + + if (StringUtils.isBlank(withCosmosDbStorageOptions.getContainerId())) { + throw new IllegalArgumentException("ContainerId is required: containerId"); + } + + Boolean compatibilityMode = withCosmosDbStorageOptions.getCompatibilityMode(); + if (compatibilityMode == null) { + withCosmosDbStorageOptions.setCompatibilityMode(true); + } + + if (StringUtils.isNotBlank(withCosmosDbStorageOptions.getKeySuffix())) { + if (withCosmosDbStorageOptions.getCompatibilityMode()) { + throw new IllegalArgumentException( + "CompatibilityMode cannot be 'true' while using a KeySuffix: withCosmosDbStorageOptions" + ); + } + + // In order to reduce key complexity, we do not allow invalid characters in a + // KeySuffix + // If the KeySuffix has invalid characters, the EscapeKey will not match + String suffixEscaped = CosmosDbKeyEscape.escapeKey(withCosmosDbStorageOptions.getKeySuffix()); + if (!withCosmosDbStorageOptions.getKeySuffix().equals(suffixEscaped)) { + throw new IllegalArgumentException( + String.format( + "Cannot use invalid Row Key characters: %s %s", + withCosmosDbStorageOptions.getKeySuffix(), + "withCosmosDbStorageOptions" + ) + ); + } + } + + cosmosDbStorageOptions = withCosmosDbStorageOptions; + + objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .findAndRegisterModules() + .enableDefaultTyping(); + + client = new DocumentClient( + cosmosDbStorageOptions.getCosmosDbEndpoint(), + cosmosDbStorageOptions.getAuthKey(), + cosmosDbStorageOptions.getConnectionPolicy(), + cosmosDbStorageOptions.getConsistencyLevel() + ); + } + + /** + * Reads storage items from storage. + * + * @param keys A collection of Ids for each item to be retrieved. + * @return A dictionary containing the retrieved items. + */ + @Override + public CompletableFuture> read(String[] keys) { + if (keys == null) { + throw new IllegalArgumentException("keys"); + } + + if (keys.length == 0) { + // No keys passed in, no result to return. + return CompletableFuture.completedFuture(new HashMap<>()); + } + + return getCollection().thenApply(collection -> { + // Issue all of the reads at once + List> documentFutures = new ArrayList<>(); + for (String key : keys) { + documentFutures.add( + getDocumentById( + CosmosDbKeyEscape.escapeKey( + key, + cosmosDbStorageOptions.getKeySuffix(), + cosmosDbStorageOptions.getCompatibilityMode() + ) + ) + ); + } + + // Map each returned Document to it's original value. + Map storeItems = new HashMap<>(); + documentFutures.forEach(documentFuture -> { + Document document = documentFuture.join(); + if (document != null) { + try { + // We store everything in a DocumentStoreItem. Get that. + JsonNode stateNode = objectMapper.readTree(document.toJson()); + DocumentStoreItem storeItem = objectMapper.treeToValue(stateNode, DocumentStoreItem.class); + + // DocumentStoreItem contains the original object. + JsonNode dataNode = objectMapper.readTree(storeItem.getDocument()); + Object item = objectMapper.treeToValue(dataNode, Class.forName(storeItem.getType())); + + if (item instanceof StoreItem) { + ((StoreItem) item).setETag(storeItem.getETag()); + } + storeItems.put(storeItem.getReadId(), item); + } catch (IOException | ClassNotFoundException e) { + logger.warn("Error reading from container", e); + } + } + }); + + return storeItems; + }); + } + + /** + * Inserts or updates one or more items into the Cosmos DB container. + * + * @param changes A dictionary of items to be inserted or updated. The + * dictionary item key is used as the ID for the inserted / + * updated item. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture write(Map changes) { + if (changes == null) { + throw new IllegalArgumentException("changes"); + } + + if (changes.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + + return getCollection().thenApply(collection -> { + for (Map.Entry change : changes.entrySet()) { + try { + ObjectNode node = objectMapper.valueToTree(change.getValue()); + + // Remove etag from JSON object that was copied from StoreItem. + // The ETag information is updated as an _etag attribute in the document + // metadata. + node.remove("eTag"); + + DocumentStoreItem documentChange = new DocumentStoreItem(); + documentChange.setId( + CosmosDbKeyEscape.escapeKey( + change.getKey(), + cosmosDbStorageOptions.getKeySuffix(), + cosmosDbStorageOptions.getCompatibilityMode() + ) + ); + documentChange.setReadId(change.getKey()); + documentChange.setDocument(node.toString()); + documentChange.setType(change.getValue().getClass().getTypeName()); + + Document document = new Document(objectMapper.writeValueAsString(documentChange)); + + RequestOptions options = new RequestOptions(); + options.setPartitionKey(new PartitionKey(documentChange.partitionKey())); + + if (change.getValue() instanceof StoreItem) { + String etag = ((StoreItem) change.getValue()).getETag(); + if (!StringUtils.isEmpty(etag)) { + // if we have an etag, do opt. concurrency replace + AccessCondition condition = new AccessCondition(); + condition.setType(AccessConditionType.IfMatch); + condition.setCondition(etag); + + options.setAccessCondition(condition); + } else if (etag != null) { + logger.warn("write change, empty eTag: " + change.getKey()); + continue; + } + } + + client.upsertDocument(collection.getSelfLink(), document, options, true); + + } catch (JsonProcessingException | DocumentClientException e) { + logger.warn("Error upserting document: " + change.getKey(), e); + if (e instanceof DocumentClientException) { + throw new RuntimeException(e.getMessage()); + } + } + } + + return null; + }); + } + + /** + * Deletes one or more items from the Cosmos DB container. + * + * @param keys An array of Ids for the items to be deleted. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture delete(String[] keys) { + if (keys == null) { + throw new IllegalArgumentException("keys"); + } + + // issue the deletes in parallel + return getCollection().thenCompose(collection -> Arrays.stream(keys).map(key -> { + String escapedKey = CosmosDbKeyEscape + .escapeKey(key, cosmosDbStorageOptions.getKeySuffix(), cosmosDbStorageOptions.getCompatibilityMode()); + return getDocumentById(escapedKey).thenApply(document -> { + if (document != null) { + try { + RequestOptions options = new RequestOptions(); + options.setPartitionKey(new PartitionKey(escapedKey)); + + client.deleteDocument(document.getSelfLink(), options); + } catch (DocumentClientException e) { + logger.warn("Unable to delete document", e); + throw new CompletionException(e); + } + } + + return null; + }); + }).collect(CompletableFutures.toFutureList()).thenApply(deleteResponses -> null)); + } + + private Database getDatabase() { + if (databaseCache == null) { + // Get the database if it exists + List databaseList = client.queryDatabases( + "SELECT * FROM root r WHERE r.id='" + cosmosDbStorageOptions.getDatabaseId() + "'", + null + ).getQueryIterable().toList(); + + if (databaseList.size() > 0) { + // Cache the database object so we won't have to query for it + // later to retrieve the selfLink. + databaseCache = databaseList.get(0); + } else { + // Create the database if it doesn't exist. + try { + Database databaseDefinition = new Database(); + databaseDefinition.setId(cosmosDbStorageOptions.getDatabaseId()); + + databaseCache = client.createDatabase(databaseDefinition, null).getResource(); + } catch (DocumentClientException e) { + // able to query or create the collection. + // Verify your connection, endpoint, and key. + logger.error("getDatabase", e); + throw new RuntimeException(e); + } + } + } + + return databaseCache; + } + + private CompletableFuture getCollection() { + if (collectionCache != null) { + return CompletableFuture.completedFuture(collectionCache); + } + + synchronized (cacheSync) { + if (collectionCache != null) { + return CompletableFuture.completedFuture(collectionCache); + } + + // Get the collection if it exists. + List collectionList = client.queryCollections( + getDatabase().getSelfLink(), + "SELECT * FROM root r WHERE r.id='" + cosmosDbStorageOptions.getContainerId() + "'", + null + ).getQueryIterable().toList(); + + if (collectionList.size() > 0) { + // Cache the collection object so we won't have to query for it + // later to retrieve the selfLink. + collectionCache = collectionList.get(0); + } else { + // Create the collection if it doesn't exist. + try { + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(cosmosDbStorageOptions.getContainerId()); + + PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition(); + partitionKeyDefinition.setPaths(Collections.singleton(DocumentStoreItem.PARTITION_KEY_PATH)); + collectionDefinition.setPartitionKey(partitionKeyDefinition); + + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(cosmosDbStorageOptions.getContainerThroughput()); + + collectionCache = client + .createCollection(getDatabase().getSelfLink(), collectionDefinition, options) + .getResource(); + } catch (DocumentClientException e) { + // able to query or create the collection. + // Verify your connection, endpoint, and key. + logger.error("getCollection", e); + throw new RuntimeException("getCollection", e); + } + } + return CompletableFuture.completedFuture(collectionCache); + } + } + + private CompletableFuture getDocumentById(String id) { + return getCollection().thenApply(collection -> { + // Retrieve the document using the DocumentClient. + List documentList = client + .queryDocuments(collection.getSelfLink(), "SELECT * FROM root r WHERE r.id='" + id + "'", null) + .getQueryIterable() + .toList(); + + if (documentList.size() > 0) { + return documentList.get(0); + } else { + return null; + } + }); + } + + /** + * Internal data structure for storing items in a CosmosDB Collection. + */ + private static class DocumentStoreItem implements StoreItem { + // PartitionKey path to be used for this document type + public static final String PARTITION_KEY_PATH = "/id"; + + @JsonProperty(value = "id") + private String id; + + @JsonProperty(value = "realId") + private String readId; + + @JsonProperty(value = "document") + private String document; + + @JsonProperty(value = "_etag") + private String eTag; + + @JsonProperty(value = "type") + private String type; + + /** + * Gets the sanitized Id/Key used as PrimaryKey. + * + * @return The ID. + */ + public String getId() { + return id; + } + + /** + * Sets the sanitized Id/Key used as PrimaryKey. + * + * @param withId The ID. + */ + public void setId(String withId) { + id = withId; + } + + /** + * Gets the un-sanitized Id/Key. + * + * @return The ID. + */ + public String getReadId() { + return readId; + } + + /** + * Sets the un-sanitized Id/Key. + * + * @param withReadId The ID. + */ + public void setReadId(String withReadId) { + readId = withReadId; + } + + /** + * Gets the persisted object. + * + * @return The item data. + */ + public String getDocument() { + return document; + } + + /** + * Sets the persisted object. + * + * @param withDocument The item data. + */ + public void setDocument(String withDocument) { + document = withDocument; + } + + /** + * Get ETag information for handling optimistic concurrency updates. + * + * @return The eTag value. + */ + @Override + public String getETag() { + return eTag; + } + + /** + * Set ETag information for handling optimistic concurrency updates. + * + * @param withETag The eTag value. + */ + @Override + public void setETag(String withETag) { + eTag = withETag; + } + + /** + * The type of the document data. + * + * @return The class name of the data being stored. + */ + public String getType() { + return type; + } + + /** + * The fully qualified class name of the data being stored. + * + * @param withType The class name of the data. + */ + public void setType(String withType) { + type = withType; + } + + /** + * The value used for the PartitionKey. + * + * @return In this case, the id field. + */ + public String partitionKey() { + return id; + } + } +} diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbPartitionedStorageOptions.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbPartitionedStorageOptions.java new file mode 100644 index 000000000..f51d2bc4b --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/CosmosDbPartitionedStorageOptions.java @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.bot.integration.Configuration; + +/** + * Cosmos DB Partitioned Storage Options. + */ +public class CosmosDbPartitionedStorageOptions { + private static final Integer DEFAULT_THROUGHPUT = 400; + private static final ConsistencyLevel DEFAULT_CONSISTENCY = ConsistencyLevel.Session; + + private String cosmosDbEndpoint; + private String authKey; + private String databaseId; + private String containerId; + private String keySuffix; + private Integer containerThroughput; + private ConnectionPolicy connectionPolicy; + private ConsistencyLevel consistencyLevel; + private Boolean compatibilityMode; + + /** + * Constructs an empty options object. + */ + public CosmosDbPartitionedStorageOptions() { + connectionPolicy = ConnectionPolicy.GetDefault(); + consistencyLevel = DEFAULT_CONSISTENCY; + containerThroughput = DEFAULT_THROUGHPUT; + } + + /** + * Construct with properties from Configuration. + * + * @param configuration The Configuration object to read properties from. + */ + public CosmosDbPartitionedStorageOptions(Configuration configuration) { + cosmosDbEndpoint = configuration.getProperty("cosmosdb.dbEndpoint"); + authKey = configuration.getProperty("cosmosdb.authKey"); + databaseId = configuration.getProperty("cosmosdb.databaseId"); + containerId = configuration.getProperty("cosmosdb.containerId"); + + // will likely need to expand this to read policy settings from Configuration. + connectionPolicy = ConnectionPolicy.GetDefault(); + + // will likely need to read consistency level from config. + consistencyLevel = DEFAULT_CONSISTENCY; + + try { + containerThroughput = Integer.parseInt(configuration.getProperty("cosmosdb.throughput")); + } catch (NumberFormatException e) { + containerThroughput = DEFAULT_THROUGHPUT; + } + } + + /** + * Gets the CosmosDB endpoint. + * + * @return The DB endpoint. + */ + public String getCosmosDbEndpoint() { + return cosmosDbEndpoint; + } + + /** + * Sets the CosmosDB endpoint. + * + * @param withCosmosDbEndpoint The DB endpoint to use. + */ + public void setCosmosDbEndpoint(String withCosmosDbEndpoint) { + cosmosDbEndpoint = withCosmosDbEndpoint; + } + + /** + * Gets the authentication key for Cosmos DB. + * + * @return The auth key for the DB. + */ + public String getAuthKey() { + return authKey; + } + + /** + * Sets the authentication key for Cosmos DB. + * + * @param withAuthKey The auth key to use. + */ + public void setAuthKey(String withAuthKey) { + authKey = withAuthKey; + } + + /** + * Gets the database identifier for Cosmos DB instance. + * + * @return The CosmosDB DB id. + */ + public String getDatabaseId() { + return databaseId; + } + + /** + * Sets the database identifier for Cosmos DB instance. + * + * @param withDatabaseId The CosmosDB id. + */ + public void setDatabaseId(String withDatabaseId) { + databaseId = withDatabaseId; + } + + /** + * Gets the container identifier. + * + * @return The container/collection ID. + */ + public String getContainerId() { + return containerId; + } + + /** + * Sets the container identifier. + * + * @param withContainerId The container/collection ID. + */ + public void setContainerId(String withContainerId) { + containerId = withContainerId; + } + + /** + * Gets the ConnectionPolicy for the CosmosDB. + * + * @return The ConnectionPolicy settings. + */ + public ConnectionPolicy getConnectionPolicy() { + return connectionPolicy; + } + + /** + * Sets the ConnectionPolicy for the CosmosDB. + * + * @param withConnectionPolicy The ConnectionPolicy settings. + */ + public void setConnectionPolicy(ConnectionPolicy withConnectionPolicy) { + connectionPolicy = withConnectionPolicy; + } + + /** + * Represents the consistency levels supported for Azure Cosmos DB client + * operations in the Azure Cosmos DB database service. + * + * The requested ConsistencyLevel must match or be weaker than that provisioned + * for the database account. Consistency levels by order of strength are Strong, + * BoundedStaleness, Session and Eventual. + * + * @return The ConsistencyLevel + */ + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + /** + * Represents the consistency levels supported for Azure Cosmos DB client + * operations in the Azure Cosmos DB database service. + * + * The requested ConsistencyLevel must match or be weaker than that provisioned + * for the database account. Consistency levels by order of strength are Strong, + * BoundedStaleness, Session and Eventual. + * + * @param withConsistencyLevel The ConsistencyLevel to use. + */ + public void setConsistencyLevel(ConsistencyLevel withConsistencyLevel) { + consistencyLevel = withConsistencyLevel; + } + + /** + * Gets the throughput set when creating the Container. Defaults to 400. + * + * @return The container throughput. + */ + public Integer getContainerThroughput() { + return containerThroughput; + } + + /** + * Sets the throughput set when creating the Container. Defaults to 400. + * + * @param withContainerThroughput The desired thoughput. + */ + public void setContainerThroughput(Integer withContainerThroughput) { + containerThroughput = withContainerThroughput; + } + + /** + * Gets a value indicating whether or not to run in Compatibility Mode. Early + * versions of CosmosDb had a key length limit of 255. Keys longer than this + * were truncated in CosmosDbKeyEscape. This remains the default behavior, but + * can be overridden by setting CompatibilityMode to false. This setting will + * also allow for using older collections where no PartitionKey was specified. + * + * Note: CompatibilityMode cannot be 'true' if KeySuffix is used. + * + * @return The compatibilityMode + */ + public Boolean getCompatibilityMode() { + return compatibilityMode; + } + + /** + * Sets a value indicating whether or not to run in Compatibility Mode. Early + * versions of CosmosDb had a key length limit of 255. Keys longer than this + * were truncated in CosmosDbKeyEscape. This remains the default behavior, but + * can be overridden by setting CompatibilityMode to false. This setting will + * also allow for using older collections where no PartitionKey was specified. + * + * Note: CompatibilityMode cannot be 'true' if KeySuffix is used. + * + * @param withCompatibilityMode Currently, max key length for cosmosdb is 1023: + * https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits + * The default for backwards compatibility is 255, + * CosmosDbKeyEscape.MaxKeyLength. + */ + public void setCompatibilityMode(Boolean withCompatibilityMode) { + this.compatibilityMode = withCompatibilityMode; + } + + /** + * Gets the suffix to be added to every key. See + * CosmosDbKeyEscape.EscapeKey(string). Note:CompatibilityMode must be set to + * 'false' to use a KeySuffix. When KeySuffix is used, keys will NOT be + * truncated but an exception will be thrown if the key length is longer than + * allowed by CosmosDb. + * + * @return String containing only valid CosmosDb key characters. (e.g. not: + * '\\', '?', '/', '#', '*'). + */ + public String getKeySuffix() { + return keySuffix; + } + + /** + * Sets the suffix to be added to every key. See + * CosmosDbKeyEscape.EscapeKey(string). Note:CompatibilityMode must be set to + * 'false' to use a KeySuffix. When KeySuffix is used, keys will NOT be + * truncated but an exception will be thrown if the key length is longer than + * allowed by CosmosDb. + * + * @param withKeySuffix String containing only valid CosmosDb key characters. + * (e.g. not: '\\', '?', '/', '#', '*'). + */ + public void setKeySuffix(String withKeySuffix) { + this.keySuffix = withKeySuffix; + } +} diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/BlobsStorage.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/BlobsStorage.java new file mode 100644 index 000000000..55d4cc331 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/BlobsStorage.java @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure.blobs; + +import com.azure.core.exception.HttpResponseException; +import com.azure.core.util.Context; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobContainerClientBuilder; +import com.azure.storage.blob.models.BlobErrorCode; +import com.azure.storage.blob.models.BlobRequestConditions; +import com.azure.storage.blob.models.BlobStorageException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.StoreItem; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpStatus; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * Implements {@link Storage} using Azure Storage Blobs. This class uses a + * single Azure Storage Blob Container. Each entity or {@link StoreItem} is + * serialized into a JSON string and stored in an individual text blob. Each + * blob is named after the store item key, which is encoded so that it conforms + * a valid blob name. an entity is an {@link StoreItem}, the storage object will + * set the entity's {@link StoreItem} property value to the blob's ETag upon + * read. Afterward, an {@link BlobRequestConditions} with the ETag value will be + * generated during Write. New entities start with a null ETag. + */ +public class BlobsStorage implements Storage { + + private ObjectMapper objectMapper; + private final BlobContainerClient containerClient; + + private final Integer millisecondsTimeout = 2000; + private final Integer retryTimes = 8; + + /** + * Initializes a new instance of the {@link BlobsStorage} class. + * + * @param dataConnectionString Azure Storage connection string. + * @param containerName Name of the Blob container where entities will be + * stored. + */ + public BlobsStorage(String dataConnectionString, String containerName) { + if (StringUtils.isBlank(dataConnectionString)) { + throw new IllegalArgumentException("dataConnectionString is required."); + } + + if (StringUtils.isBlank(containerName)) { + throw new IllegalArgumentException("containerName is required."); + } + + objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .findAndRegisterModules() + .enableDefaultTyping(); + + containerClient = new BlobContainerClientBuilder().connectionString(dataConnectionString) + .containerName(containerName) + .buildClient(); + } + + /** + * Deletes entity blobs from the configured container. + * + * @param keys An array of entity keys. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture delete(String[] keys) { + if (keys == null) { + throw new IllegalArgumentException("The 'keys' parameter is required."); + } + + for (String key : keys) { + String blobName = getBlobName(key); + BlobClient blobClient = containerClient.getBlobClient(blobName); + if (blobClient.exists()) { + try { + blobClient.delete(); + } catch (BlobStorageException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Retrieve entities from the configured blob container. + * + * @param keys An array of entity keys. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture> read(String[] keys) { + if (keys == null) { + throw new IllegalArgumentException("The 'keys' parameter is required."); + } + + if (!containerClient.exists()) { + try { + containerClient.create(); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + Map items = new HashMap<>(); + + for (String key : keys) { + String blobName = getBlobName(key); + BlobClient blobClient = containerClient.getBlobClient(blobName); + innerReadBlob(blobClient).thenAccept(value -> { + if (value != null) { + items.put(key, value); + } + }); + } + return CompletableFuture.completedFuture(items); + } + + /** + * Stores a new entity in the configured blob container. + * + * @param changes The changes to write to storage. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture write(Map changes) { + if (changes == null) { + throw new IllegalArgumentException("The 'changes' parameter is required."); + } + + if (!containerClient.exists()) { + try { + containerClient.create(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + for (Map.Entry keyValuePair : changes.entrySet()) { + Object newValue = keyValuePair.getValue(); + StoreItem storeItem = newValue instanceof StoreItem ? (StoreItem) newValue : null; + + // "*" eTag in StoreItem converts to null condition for AccessCondition + boolean isNullOrEmpty = + storeItem == null || StringUtils.isBlank(storeItem.getETag()) || storeItem.getETag().equals("*"); + BlobRequestConditions accessCondition = + !isNullOrEmpty ? new BlobRequestConditions().setIfMatch(storeItem.getETag()) : null; + + String blobName = getBlobName(keyValuePair.getKey()); + BlobClient blobReference = containerClient.getBlobClient(blobName); + try { + String json = objectMapper.writeValueAsString(newValue); + InputStream stream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + // verify the corresponding length + blobReference.uploadWithResponse( + stream, + stream.available(), + null, + null, + null, + null, + accessCondition, + null, + Context.NONE + ); + } catch (HttpResponseException e) { + if (e.getResponse().getStatusCode() == HttpStatus.SC_BAD_REQUEST) { + StringBuilder sb = + new StringBuilder("An error occurred while trying to write an object. The underlying "); + sb.append(BlobErrorCode.INVALID_BLOCK_LIST); + sb.append( + " error is commonly caused due to " + + "concurrently uploading an object larger than 128MB in size." + ); + + throw new HttpResponseException(sb.toString(), e.getResponse()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + return CompletableFuture.completedFuture(null); + } + + private static String getBlobName(String key) { + if (StringUtils.isBlank(key)) { + throw new IllegalArgumentException("The 'key' parameter is required."); + } + + String blobName; + try { + blobName = URLEncoder.encode(key, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("The key could not be encoded"); + } + + return blobName; + } + + private CompletableFuture innerReadBlob(BlobClient blobReference) { + Integer i = 0; + while (true) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + blobReference.download(outputStream); + String contentString = outputStream.toString(); + + Object obj; + // We are doing this try/catch because we are receiving String or HashMap + try { + // We need to deserialize to an Object class since there are contentString which + // has an Object type + obj = objectMapper.readValue(contentString, Object.class); + } catch (MismatchedInputException ex) { + // In case of the contentString has the structure of a HashMap, + // we need to deserialize it to a HashMap object + obj = objectMapper.readValue(contentString, HashMap.class); + } + + if (obj instanceof StoreItem) { + String eTag = blobReference.getProperties().getETag(); + ((StoreItem) obj).setETag(eTag); + } + + return CompletableFuture.completedFuture(obj); + } catch (HttpResponseException e) { + if (e.getResponse().getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED) { + // additional retry logic, + // even though this is a read operation blob storage can return 412 if there is + // contention + if (i++ < retryTimes) { + try { + TimeUnit.MILLISECONDS.sleep(millisecondsTimeout); + continue; + } catch (InterruptedException ex) { + break; + } + } + throw e; + } else { + break; + } + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + return CompletableFuture.completedFuture(null); + } +} diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/BlobsTranscriptStore.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/BlobsTranscriptStore.java new file mode 100644 index 000000000..76b78e837 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/BlobsTranscriptStore.java @@ -0,0 +1,520 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure.blobs; + +import com.azure.core.exception.HttpResponseException; +import com.azure.core.http.rest.PagedResponse; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobContainerClientBuilder; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.BlobListDetails; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.microsoft.bot.builder.BotAssert; +import com.microsoft.bot.builder.PagedResult; +import com.microsoft.bot.builder.TranscriptInfo; +import com.microsoft.bot.builder.TranscriptStore; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.Pair; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpStatus; + +import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * The blobs transcript store stores transcripts in an Azure Blob container. + * Each activity is stored as json blob in structure of + * container/{channelId]/{conversationId}/{Timestamp.ticks}-{activity.id}.json. + */ +public class BlobsTranscriptStore implements TranscriptStore { + + // Containers checked for creation. + private static final HashSet CHECKED_CONTAINERS = new HashSet(); + + private final Integer milisecondsTimeout = 2000; + private final Integer retryTimes = 3; + private final Integer longRadix = 16; + private final Integer multipleProductValue = 10_000_000; + + private final ObjectMapper jsonSerializer; + private BlobContainerClient containerClient; + + /** + * Initializes a new instance of the {@link BlobsTranscriptStore} class. + * + * @param dataConnectionString Azure Storage connection string. + * @param containerName Name of the Blob container where entities will be + * stored. + */ + public BlobsTranscriptStore(String dataConnectionString, String containerName) { + if (StringUtils.isBlank(dataConnectionString)) { + throw new IllegalArgumentException("dataConnectionString"); + } + + if (StringUtils.isBlank(containerName)) { + throw new IllegalArgumentException("containerName"); + } + + jsonSerializer = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL) + .enable(SerializationFeature.INDENT_OUTPUT) + .findAndRegisterModules(); + + // Triggers a check for the existence of the container + containerClient = this.getContainerClient(dataConnectionString, containerName); + } + + /** + * Log an activity to the transcript. + * + * @param activity Activity being logged. + * @return A CompletableFuture that represents the work queued to execute. + */ + public CompletableFuture logActivity(Activity activity) { + BotAssert.activityNotNull(activity); + + switch (activity.getType()) { + case ActivityTypes.MESSAGE_UPDATE: + Activity updatedActivity = null; + try { + updatedActivity = + jsonSerializer.readValue(jsonSerializer.writeValueAsString(activity), Activity.class); + } catch (IOException ex) { + ex.printStackTrace(); + } + updatedActivity.setType(ActivityTypes.MESSAGE); // fixup original type (should be Message) + Activity finalUpdatedActivity = updatedActivity; + innerReadBlob(activity).thenAccept(activityAndBlob -> { + if (activityAndBlob != null && activityAndBlob.getLeft() != null) { + finalUpdatedActivity.setLocalTimestamp(activityAndBlob.getLeft().getLocalTimestamp()); + finalUpdatedActivity.setTimestamp(activityAndBlob.getLeft().getTimestamp()); + logActivityToBlobClient(finalUpdatedActivity, activityAndBlob.getRight(), true) + .thenApply(task -> CompletableFuture.completedFuture(null)); + } else { + // The activity was not found, so just add a record of this update. + this.innerLogActivity(finalUpdatedActivity) + .thenApply(task -> CompletableFuture.completedFuture(null)); + } + }); + + return CompletableFuture.completedFuture(null); + + case ActivityTypes.MESSAGE_DELETE: + innerReadBlob(activity).thenAccept(activityAndBlob -> { + if (activityAndBlob != null && activityAndBlob.getLeft() != null) { + ChannelAccount from = new ChannelAccount(); + from.setId("deleted"); + from.setRole(activityAndBlob.getLeft().getFrom().getRole()); + ChannelAccount recipient = new ChannelAccount(); + recipient.setId("deleted"); + recipient.setRole(activityAndBlob.getLeft().getRecipient().getRole()); + + // tombstone the original message + Activity tombstonedActivity = new Activity(ActivityTypes.MESSAGE_DELETE); + tombstonedActivity.setId(activityAndBlob.getLeft().getId()); + tombstonedActivity.setFrom(from); + tombstonedActivity.setRecipient(recipient); + tombstonedActivity.setLocale(activityAndBlob.getLeft().getLocale()); + tombstonedActivity.setLocalTimestamp(activityAndBlob.getLeft().getTimestamp()); + tombstonedActivity.setTimestamp(activityAndBlob.getLeft().getTimestamp()); + tombstonedActivity.setChannelId(activityAndBlob.getLeft().getChannelId()); + tombstonedActivity.setConversation(activityAndBlob.getLeft().getConversation()); + tombstonedActivity.setServiceUrl(activityAndBlob.getLeft().getServiceUrl()); + tombstonedActivity.setReplyToId(activityAndBlob.getLeft().getReplyToId()); + + logActivityToBlobClient(tombstonedActivity, activityAndBlob.getRight(), true) + .thenApply(task -> CompletableFuture.completedFuture(null)); + } + }); + + return CompletableFuture.completedFuture(null); + + default: + this.innerLogActivity(activity).thenApply(task -> CompletableFuture.completedFuture(null)); + return CompletableFuture.completedFuture(null); + } + } + + /** + * Get activities for a conversation (Aka the transcript). + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @param continuationToken The continuation token (if available). + * @param startDate A cutoff date. Activities older than this date are + * not included. + * @return PagedResult of activities. + */ + public CompletableFuture> getTranscriptActivities( + String channelId, + String conversationId, + @Nullable String continuationToken, + OffsetDateTime startDate + ) { + if (startDate == null) { + startDate = OffsetDateTime.MIN; + } + + final int pageSize = 20; + + if (StringUtils.isBlank(channelId)) { + throw new IllegalArgumentException("Missing channelId"); + } + + if (StringUtils.isBlank(conversationId)) { + throw new IllegalArgumentException("Missing conversationId"); + } + + PagedResult pagedResult = new PagedResult(); + + String token = null; + List blobs = new ArrayList(); + do { + String prefix = String.format("%s/%s/", sanitizeKey(channelId), sanitizeKey(conversationId)); + Iterable> resultSegment = containerClient + .listBlobsByHierarchy("/", this.getOptionsWithMetadata(prefix), null) + .iterableByPage(token); + token = null; + for (PagedResponse blobPage : resultSegment) { + for (BlobItem blobItem : blobPage.getValue()) { + OffsetDateTime parseDateTime = OffsetDateTime.parse(blobItem.getMetadata().get("Timestamp")); + if (parseDateTime.isAfter(startDate) || parseDateTime.isEqual(startDate)) { + if (continuationToken != null) { + if (blobItem.getName().equals(continuationToken)) { + // we found continuation token + continuationToken = null; + } + } else { + blobs.add(blobItem); + if (blobs.size() == pageSize) { + break; + } + } + } + } + + // Get the continuation token and loop until it is empty. + token = blobPage.getContinuationToken(); + } + } while (!StringUtils.isBlank(token) && blobs.size() < pageSize); + + pagedResult.setItems(blobs.stream().map(bl -> { + BlobClient blobClient = containerClient.getBlobClient(bl.getName()); + return this.getActivityFromBlobClient(blobClient); + }).map(t -> t.join()).collect(Collectors.toList())); + + if (pagedResult.getItems().size() == pageSize) { + pagedResult.setContinuationToken(blobs.get(blobs.size() - 1).getName()); + } + + return CompletableFuture.completedFuture(pagedResult); + } + + /** + * List conversations in the channelId. + * + * @param channelId The ID of the channel. + * @param continuationToken The continuation token (if available). + * @return A CompletableFuture that represents the work queued to execute. + */ + public CompletableFuture> listTranscripts( + String channelId, + @Nullable String continuationToken + ) { + final int pageSize = 20; + + if (StringUtils.isBlank(channelId)) { + throw new IllegalArgumentException("Missing channelId"); + } + + String token = null; + + List conversations = new ArrayList(); + do { + String prefix = String.format("%s/", sanitizeKey(channelId)); + Iterable> resultSegment = containerClient + .listBlobsByHierarchy("/", this.getOptionsWithMetadata(prefix), null) + .iterableByPage(token); + token = null; + for (PagedResponse blobPage : resultSegment) { + for (BlobItem blobItem : blobPage.getValue()) { + // Unescape the Id we escaped when we saved it + String conversationId = new String(); + String lastName = Arrays.stream(blobItem.getName().split("/")) + .reduce((first, second) -> second.length() > 0 ? second : first) + .get(); + try { + conversationId = URLDecoder.decode(lastName, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException ex) { + ex.printStackTrace(); + } + TranscriptInfo conversation = + new TranscriptInfo(conversationId, channelId, blobItem.getProperties().getCreationTime()); + if (continuationToken != null) { + if (StringUtils.equals(conversation.getId(), continuationToken)) { + // we found continuation token + continuationToken = null; + } + + // skip record + } else { + conversations.add(conversation); + if (conversations.size() == pageSize) { + break; + } + } + } + } + } while (!StringUtils.isBlank(token) && conversations.size() < pageSize); + + PagedResult pagedResult = new PagedResult(); + pagedResult.setItems(conversations); + + if (pagedResult.getItems().size() == pageSize) { + pagedResult.setContinuationToken(pagedResult.getItems().get(pagedResult.getItems().size() - 1).getId()); + } + + return CompletableFuture.completedFuture(pagedResult); + } + + /** + * Delete a specific conversation and all of it's activities. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation to delete. + * @return A CompletableFuture that represents the work queued to execute. + */ + public CompletableFuture deleteTranscript(String channelId, String conversationId) { + if (StringUtils.isBlank(channelId)) { + throw new IllegalArgumentException("Missing channelId"); + } + + if (StringUtils.isBlank(conversationId)) { + throw new IllegalArgumentException("Missing conversationId"); + } + + String token = null; + do { + String prefix = String.format("%s/%s/", sanitizeKey(channelId), sanitizeKey(conversationId)); + Iterable> resultSegment = containerClient + .listBlobsByHierarchy("/", this.getOptionsWithMetadata(prefix), null) + .iterableByPage(token); + token = null; + + for (PagedResponse blobPage : resultSegment) { + for (BlobItem blobItem : blobPage.getValue()) { + BlobClient blobClient = containerClient.getBlobClient(blobItem.getName()); + if (blobClient.exists()) { + try { + blobClient.delete(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + // Get the continuation token and loop until it is empty. + token = blobPage.getContinuationToken(); + } + } + } while (!StringUtils.isBlank(token)); + + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture> innerReadBlob(Activity activity) { + int i = 0; + while (true) { + try { + String token = null; + do { + String prefix = String.format( + "%s/%s/", + sanitizeKey(activity.getChannelId()), + sanitizeKey(activity.getConversation().getId()) + ); + Iterable> resultSegment = containerClient + .listBlobsByHierarchy("/", this.getOptionsWithMetadata(prefix), null) + .iterableByPage(token); + token = null; + for (PagedResponse blobPage : resultSegment) { + for (BlobItem blobItem : blobPage.getValue()) { + if (blobItem.getMetadata().get("Id").equals(activity.getId())) { + BlobClient blobClient = containerClient.getBlobClient(blobItem.getName()); + return this.getActivityFromBlobClient( + blobClient + ).thenApply(blobActivity -> new Pair(blobActivity, blobClient)); + } + } + + // Get the continuation token and loop until it is empty. + token = blobPage.getContinuationToken(); + } + } while (!StringUtils.isBlank(token)); + + return CompletableFuture.completedFuture(null); + } catch (HttpResponseException ex) { + if (ex.getResponse().getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED) { + // additional retry logic, + // even though this is a read operation blob storage can return 412 if there is + // contention + if (i++ < retryTimes) { + try { + TimeUnit.MILLISECONDS.sleep(milisecondsTimeout); + continue; + } catch (InterruptedException e) { + break; + } + } + throw ex; + } + // This break will finish the while when the catch if condition is false + break; + } + } + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture getActivityFromBlobClient(BlobClient blobClient) { + ByteArrayOutputStream content = new ByteArrayOutputStream(); + blobClient.download(content); + String contentString = new String(content.toByteArray()); + try { + return CompletableFuture.completedFuture(jsonSerializer.readValue(contentString, Activity.class)); + } catch (IOException ex) { + return CompletableFuture.completedFuture(null); + } + } + + private CompletableFuture innerLogActivity(Activity activity) { + String blobName = this.getBlobName(activity); + BlobClient blobClient = containerClient.getBlobClient(blobName); + return logActivityToBlobClient(activity, blobClient, null); + } + + private CompletableFuture logActivityToBlobClient( + Activity activity, + BlobClient blobClient, + Boolean overwrite + ) { + if (overwrite == null) { + overwrite = false; + } + String activityJson = null; + try { + activityJson = jsonSerializer.writeValueAsString(activity); + } catch (IOException ex) { + ex.printStackTrace(); + } + InputStream data = new ByteArrayInputStream(activityJson.getBytes(StandardCharsets.UTF_8)); + + try { + blobClient.upload(data, data.available(), overwrite); + } catch (IOException ex) { + ex.printStackTrace(); + } + Map metaData = new HashMap(); + metaData.put("Id", activity.getId()); + if (activity.getFrom() != null) { + metaData.put("FromId", activity.getFrom().getId()); + } + + if (activity.getRecipient() != null) { + metaData.put("RecipientId", activity.getRecipient().getId()); + } + metaData.put("Timestamp", activity.getTimestamp().toString()); + + blobClient.setMetadata(metaData); + + return CompletableFuture.completedFuture(null); + } + + private String getBlobName(Activity activity) { + String blobName = String.format( + "%s/%s/%s-%s.json", + sanitizeKey(activity.getChannelId()), + sanitizeKey(activity.getConversation().getId()), + this.formatTicks(activity.getTimestamp()), + sanitizeKey(activity.getId()) + ); + + return blobName; + } + + private String sanitizeKey(String key) { + // Blob Name rules: case-sensitive any url char + try { + return URLEncoder.encode(key, StandardCharsets.UTF_8.name()); + } catch (Exception ex) { + ex.printStackTrace(); + } + return ""; + } + + private BlobContainerClient getContainerClient(String dataConnectionString, String containerName) { + containerName = containerName.toLowerCase(); + containerClient = new BlobContainerClientBuilder().connectionString(dataConnectionString) + .containerName(containerName) + .buildClient(); + if (!CHECKED_CONTAINERS.contains(containerName)) { + CHECKED_CONTAINERS.add(containerName); + if (!containerClient.exists()) { + try { + containerClient.create(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } + return containerClient; + } + + /** + * Formats a timestamp in a way that is consistent with the C# SDK. + * + * @param dateTime The dateTime used to get the ticks + * @return The String representing the ticks. + */ + private String formatTicks(OffsetDateTime dateTime) { + final Instant begin = ZonedDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant(); + final Instant end = dateTime.toInstant(); + long secsDiff = Math.subtractExact(end.getEpochSecond(), begin.getEpochSecond()); + long totalHundredNanos = Math.multiplyExact(secsDiff, multipleProductValue); + final Long ticks = Math.addExact(totalHundredNanos, (end.getNano() - begin.getNano()) / 100); + return Long.toString(ticks, longRadix); + } + + private ListBlobsOptions getOptionsWithMetadata(String prefix) { + BlobListDetails details = new BlobListDetails(); + details.setRetrieveMetadata(true); + ListBlobsOptions options = new ListBlobsOptions(); + options.setDetails(details); + options.setPrefix(prefix); + return options; + } +} diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/package-info.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/package-info.java new file mode 100644 index 000000000..a3bfa207f --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/blobs/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.azure.blobs. + */ +package com.microsoft.bot.azure.blobs; diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/package-info.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/package-info.java new file mode 100644 index 000000000..a96f4b587 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.azure. + */ +@Deprecated +package com.microsoft.bot.azure; diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/AzureQueueStorage.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/AzureQueueStorage.java new file mode 100644 index 000000000..e839e7c85 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/AzureQueueStorage.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure.queues; + +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.QueueClientBuilder; +import com.azure.storage.queue.models.SendMessageResult; +import com.microsoft.bot.builder.QueueStorage; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; + +/** + * Service used to add messages to an Azure.Storage.Queues. + */ +public class AzureQueueStorage extends QueueStorage { + private Boolean createQueueIfNotExists = true; + private final QueueClient queueClient; + + /** + * Initializes a new instance of the {@link AzureQueueStorage} class. + * + * @param queuesStorageConnectionString Azure Storage connection string. + * @param queueName Name of the storage queue where entities + * will be queued. + */ + public AzureQueueStorage(String queuesStorageConnectionString, String queueName) { + if (StringUtils.isBlank(queuesStorageConnectionString)) { + throw new IllegalArgumentException("queuesStorageConnectionString is required."); + } + + if (StringUtils.isBlank(queueName)) { + throw new IllegalArgumentException("queueName is required."); + } + + queueClient = + new QueueClientBuilder().connectionString(queuesStorageConnectionString).queueName(queueName).buildClient(); + } + + /** + * Queue an Activity to an Azure.Storage.Queues.QueueClient. The visibility + * timeout specifies how long the message should be invisible to Dequeue and + * Peek operations. The message content must be a UTF-8 encoded string that is + * up to 64KB in size. + * + * @param activity This is expected to be an {@link Activity} retrieved + * from a call to + * activity.GetConversationReference().GetContinuationActivity(). + * This enables restarting the conversation using + * BotAdapter.ContinueConversationAsync. + * @param visibilityTimeout Default value of 0. Cannot be larger than 7 days. + * @param timeToLive Specifies the time-to-live interval for the message. + * @return {@link SendMessageResult} as a Json string, from the QueueClient + * SendMessageAsync operation. + */ + @Override + public CompletableFuture queueActivity( + Activity activity, + @Nullable Duration visibilityTimeout, + @Nullable Duration timeToLive + ) { + return CompletableFuture.supplyAsync(() -> { + if (createQueueIfNotExists) { + try { + queueClient.create(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // This is an optimization flag to check if the container creation call has been + // made. + // It is okay if this is called more than once. + createQueueIfNotExists = false; + } + + try { + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + String serializedActivity = jacksonAdapter.serialize(activity); + byte[] encodedBytes = serializedActivity.getBytes(StandardCharsets.UTF_8); + String encodedString = Base64.getEncoder().encodeToString(encodedBytes); + + SendMessageResult receipt = queueClient.sendMessage(encodedString); + return jacksonAdapter.serialize(receipt); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + }); + } +} diff --git a/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/package-info.java b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/package-info.java new file mode 100644 index 000000000..6e6a29c76 --- /dev/null +++ b/libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.azure.queues. + */ +package com.microsoft.bot.azure.queues; diff --git a/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureEmulatorUtils.java b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureEmulatorUtils.java new file mode 100644 index 000000000..ca79b74ac --- /dev/null +++ b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureEmulatorUtils.java @@ -0,0 +1,36 @@ +package com.microsoft.bot.azure; + +import java.io.File; +import java.io.IOException; + +public class AzureEmulatorUtils { + + private static boolean isStorageEmulatorAvailable = false; + private static boolean hasStorageEmulatorBeenTested = false; + private static final String NO_EMULATOR_MESSAGE = "This test requires Azure STORAGE Emulator! Go to https://docs.microsoft.com/azure/storage/common/storage-use-emulator to download and install."; + + public static boolean isStorageEmulatorAvailable() { + if (!hasStorageEmulatorBeenTested) { + try { + File emulator = new File(System.getenv("ProgramFiles") + " (x86)\\Microsoft SDKs\\Azure\\Storage Emulator\\AzureStorageEmulator.exe"); + if (emulator.exists()) { + Process p = Runtime.getRuntime().exec("cmd /C \"" + System.getenv("ProgramFiles") + + " (x86)\\Microsoft SDKs\\Azure\\Storage Emulator\\AzureStorageEmulator.exe\" start"); + int result = p.waitFor(); + // status = 0: the service was started. + // status = -5: the service is already started. Only one instance of the + // application + // can be run at the same time. + isStorageEmulatorAvailable = result == 0 || result == -5; + } else { + isStorageEmulatorAvailable = false; + } + } catch (IOException | InterruptedException ex) { + isStorageEmulatorAvailable = false; + System.out.println(NO_EMULATOR_MESSAGE); + } + hasStorageEmulatorBeenTested = true; + } + return isStorageEmulatorAvailable; + } +} diff --git a/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureQueueTests.java b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureQueueTests.java new file mode 100644 index 000000000..eb51bba8a --- /dev/null +++ b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureQueueTests.java @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.QueueClientBuilder; +import com.azure.storage.queue.models.QueueMessageItem; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.azure.queues.AzureQueueStorage; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MemoryStorage; +import com.microsoft.bot.builder.QueueStorage; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogManager; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityEventNames; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationReference; +import org.apache.commons.codec.binary.Base64; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.Calendar; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.restclient.serializer.JacksonAdapter; + +public class AzureQueueTests { + private static final Integer DEFAULT_DELAY = 2000; + private final String connectionString = "UseDevelopmentStorage=true"; + + // These tests require Azure Storage Emulator v5.7 + public QueueClient containerInit(String name) { + QueueClient queue = new QueueClientBuilder() + .connectionString(connectionString) + .queueName(name) + .buildClient(); + queue.create(); + queue.clearMessages(); + return queue; + } + + @Before + public void beforeTest() { + org.junit.Assume.assumeTrue(AzureEmulatorUtils.isStorageEmulatorAvailable()); + } + + @Test + public void continueConversationLaterTests() { + String queueName = "continueconversationlatertests"; + QueueClient queue = containerInit(queueName); + + ConversationReference cr = TestAdapter.createConversationReference("ContinueConversationLaterTests", "User1", "Bot"); + TestAdapter adapter = new TestAdapter(cr) + .useStorage(new MemoryStorage()) + .useBotState(new ConversationState(new MemoryStorage()), new UserState(new MemoryStorage())); + + AzureQueueStorage queueStorage = new AzureQueueStorage(connectionString, queueName); + + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.SECOND, 2); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + ContinueConversationLater ccl = new ContinueConversationLater(); + ccl.setDate(sdf.format(cal.getTime())); + ccl.setValue("foo"); + DialogManager dm = new DialogManager(ccl, "DialogStateProperty"); + dm.getInitialTurnState().replace("QueueStorage", queueStorage); + + new TestFlow(adapter, turnContext -> CompletableFuture.runAsync(() -> dm.onTurn(turnContext))) + .send("hi") + .startTest().join(); + + try { + Thread.sleep(DEFAULT_DELAY); + } catch (InterruptedException e) { + e.printStackTrace(); + Assert.fail(); + } + + QueueMessageItem messages = queue.receiveMessage(); + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + String messageJson = new String(Base64.decodeBase64(messages.getMessageText())); + Activity activity = null; + + try { + activity = jacksonAdapter.deserialize(messageJson, Activity.class); + } catch (IOException e) { + e.printStackTrace(); + Assert.fail(); + } + + Assert.assertTrue(activity.isType(ActivityTypes.EVENT)); + Assert.assertEquals(ActivityEventNames.CONTINUE_CONVERSATION, activity.getName()); + Assert.assertEquals("foo", activity.getValue()); + Assert.assertNotNull(activity.getRelatesTo()); + ConversationReference cr2 = activity.getConversationReference(); + cr.setActivityId(null); + cr2.setActivityId(null); + + try { + Assert.assertEquals(jacksonAdapter.serialize(cr), jacksonAdapter.serialize(cr2)); + } catch (IOException e) { + e.printStackTrace(); + Assert.fail(); + } + } + + private class ContinueConversationLater extends Dialog { + @JsonProperty("disabled") + private Boolean disabled = false; + + @JsonProperty("date") + private String date; + + @JsonProperty("value") + private String value; + + /** + * Initializes a new instance of the Dialog class. + */ + public ContinueConversationLater() { + super(ContinueConversationLater.class.getName()); + } + + @Override + public CompletableFuture beginDialog(DialogContext dc, Object options) { + if (this.disabled) { + return dc.endDialog(); + } + + String dateString = this.date; + LocalDateTime date = null; + try { + date = LocalDateTime.parse(dateString); + } catch (DateTimeParseException ex) { + return Async.completeExceptionally(new IllegalArgumentException("Date is invalid")); + } + + ZonedDateTime zonedDate = date.atZone(ZoneOffset.UTC); + ZonedDateTime now = LocalDateTime.now().atZone(ZoneOffset.UTC); + if (zonedDate.isBefore(now)) { + return Async.completeExceptionally(new IllegalArgumentException("Date must be in the future")); + } + + // create ContinuationActivity from the conversation reference. + Activity activity = dc.getContext().getActivity().getConversationReference().getContinuationActivity(); + activity.setValue(this.value); + + Duration visibility = Duration.between(zonedDate, now); + Duration ttl = visibility.plusMinutes(2); + + QueueStorage queueStorage = dc.getContext().getTurnState().get("QueueStorage"); + if (queueStorage == null) { + return Async.completeExceptionally(new NullPointerException("Unable to locate QueueStorage in HostContext")); + } + return queueStorage.queueActivity(activity, visibility, ttl).thenCompose(receipt -> { + // return the receipt as the result + return dc.endDialog(receipt); + }); + } + + /** + * Gets an optional expression which if is true will disable this action. + * "user.age > 18". + * @return A boolean expression. + */ + public Boolean getDisabled() { + return disabled; + } + + /** + * Sets an optional expression which if is true will disable this action. + * "user.age > 18". + * @param withDisabled A boolean expression. + */ + public void setDisabled(Boolean withDisabled) { + this.disabled = withDisabled; + } + + /** + * Gets the expression which resolves to the date/time to continue the conversation. + * @return Date/time string in ISO 8601 format to continue conversation. + */ + public String getDate() { + return date; + } + + /** + * Sets the expression which resolves to the date/time to continue the conversation. + * @param withDate Date/time string in ISO 8601 format to continue conversation. + */ + public void setDate(String withDate) { + this.date = withDate; + } + + /** + * Gets an optional value to use for EventActivity.Value. + * @return The value to use for the EventActivity.Value payload. + */ + public String getValue() { + return value; + } + + /** + * Sets an optional value to use for EventActivity.Value. + * @param withValue The value to use for the EventActivity.Value payload. + */ + public void setValue(String withValue) { + this.value = withValue; + } + } +} diff --git a/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/CosmosDBKeyEscapeTests.java b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/CosmosDBKeyEscapeTests.java new file mode 100644 index 000000000..1a1d11c4f --- /dev/null +++ b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/CosmosDBKeyEscapeTests.java @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import org.junit.Assert; +import org.junit.Test; + +public class CosmosDBKeyEscapeTests { + @Test(expected = IllegalArgumentException.class) + public void sanitizeKeyShouldFailWithNullKey() { + // Null key should throw + CosmosDbKeyEscape.escapeKey(null); + } + + @Test(expected = IllegalArgumentException.class) + public void sanitizeKeyShouldFailWithEmptyKey() { + // Empty string should throw + CosmosDbKeyEscape.escapeKey(new String()); + } + + @Test(expected = IllegalArgumentException.class) + public void sanitizeKeyShouldFailWithWhitespaceKey() { + // Whitespace key should throw + CosmosDbKeyEscape.escapeKey(" "); + } + + @Test + public void sanitizeKeyShouldNotChangeAValidKey() { + String validKey = "Abc12345"; + String sanitizedKey = CosmosDbKeyEscape.escapeKey(validKey); + Assert.assertEquals(validKey, sanitizedKey); + } + + @Test + public void longKeyShouldBeTruncated() { + StringBuilder tooLongKey = new StringBuilder(); + for (int i = 0; i < CosmosDbKeyEscape.MAX_KEY_LENGTH + 1; i++) { + tooLongKey.append("a"); + } + + String sanitizedKey = CosmosDbKeyEscape.escapeKey(tooLongKey.toString()); + Assert.assertTrue(sanitizedKey.length() <= CosmosDbKeyEscape.MAX_KEY_LENGTH); + + // The resulting key should be: + String hash = String.format("%x", tooLongKey.toString().hashCode()); + String correctKey = sanitizedKey.substring(0, CosmosDbKeyEscape.MAX_KEY_LENGTH - hash.length()) + hash; + + Assert.assertEquals(correctKey, sanitizedKey); + } + + @Test + public void longKeyWithIllegalCharactersShouldBeTruncated() { + StringBuilder tooLongKey = new StringBuilder(); + for (int i = 0; i < CosmosDbKeyEscape.MAX_KEY_LENGTH + 1; i++) { + tooLongKey.append("a"); + } + + String tooLongKeyWithIllegalCharacters = "?test?" + tooLongKey.toString(); + String sanitizedKey = CosmosDbKeyEscape.escapeKey(tooLongKeyWithIllegalCharacters); + + // Verify the key ws truncated + Assert.assertTrue(sanitizedKey.length() <= CosmosDbKeyEscape.MAX_KEY_LENGTH); + + // Make sure the escaping still happened + Assert.assertTrue(sanitizedKey.startsWith("*3ftest*3f")); + } + + @Test + public void sanitizeKeyShouldEscapeIllegalCharacter() + { + // Ascii code of "?" is "3f". + String sanitizedKey = CosmosDbKeyEscape.escapeKey("?test?"); + Assert.assertEquals("*3ftest*3f", sanitizedKey); + + // Ascii code of "/" is "2f". + String sanitizedKey2 = CosmosDbKeyEscape.escapeKey("/test/"); + Assert.assertEquals("*2ftest*2f", sanitizedKey2); + + // Ascii code of "\" is "5c". + String sanitizedKey3 = CosmosDbKeyEscape.escapeKey("\\test\\"); + Assert.assertEquals("*5ctest*5c", sanitizedKey3); + + // Ascii code of "#" is "23". + String sanitizedKey4 = CosmosDbKeyEscape.escapeKey("#test#"); + Assert.assertEquals("*23test*23", sanitizedKey4); + + // Ascii code of "*" is "2a". + String sanitizedKey5 = CosmosDbKeyEscape.escapeKey("*test*"); + Assert.assertEquals("*2atest*2a", sanitizedKey5); + + // Check a compound key + String compoundSanitizedKey = CosmosDbKeyEscape.escapeKey("?#/"); + Assert.assertEquals("*3f*23*2f", compoundSanitizedKey); + } + + @Test + public void collisionsShouldNotHappen() + { + String validKey = "*2atest*2a"; + String validKey2 = "*test*"; + + // If we failed to esacpe the "*", then validKey2 would + // escape to the same value as validKey. To prevent this + // we makes sure to escape the *. + + // Ascii code of "*" is "2a". + String escaped1 = CosmosDbKeyEscape.escapeKey(validKey); + String escaped2 = CosmosDbKeyEscape.escapeKey(validKey2); + + Assert.assertNotEquals(escaped1, escaped2); + } + + @Test + public void longKeyShouldNotBeTruncatedWithFalseCompatibilityMode() { + StringBuilder tooLongKey = new StringBuilder(); + for (int i = 0; i < CosmosDbKeyEscape.MAX_KEY_LENGTH + 1; i++) { + tooLongKey.append("a"); + } + + String sanitizedKey = CosmosDbKeyEscape.escapeKey(tooLongKey.toString(), new String(), false); + Assert.assertEquals(CosmosDbKeyEscape.MAX_KEY_LENGTH + 1, sanitizedKey.length()); + + // The resulting key should be identical + Assert.assertEquals(tooLongKey.toString(), sanitizedKey); + } + + @Test + public void longKeyWithIllegalCharactersShouldNotBeTruncatedWithFalseCompatibilityMode() + { + StringBuilder tooLongKey = new StringBuilder(); + for (int i = 0; i < CosmosDbKeyEscape.MAX_KEY_LENGTH + 1; i++) { + tooLongKey.append("a"); + } + + String longKeyWithIllegalCharacters = "?test?" + tooLongKey.toString(); + String sanitizedKey = CosmosDbKeyEscape.escapeKey(longKeyWithIllegalCharacters, new String(), false); + + // Verify the key was NOT truncated + Assert.assertEquals(longKeyWithIllegalCharacters.length() + 4, sanitizedKey.length()); + + // Make sure the escaping still happened + Assert.assertTrue(sanitizedKey.startsWith("*3ftest*3f")); + } + + @Test + public void keySuffixIsAddedToEndOfKey() + { + String suffix = "test suffix"; + String key = "this is a test"; + String sanitizedKey = CosmosDbKeyEscape.escapeKey(key, suffix, false); + + // Verify the suffix was added to the end of the key + Assert.assertEquals(sanitizedKey, key + suffix); + } +} diff --git a/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/CosmosDbPartitionStorageTests.java b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/CosmosDbPartitionStorageTests.java new file mode 100644 index 000000000..86e91e4f8 --- /dev/null +++ b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/CosmosDbPartitionStorageTests.java @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DocumentClient; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.StorageBaseTests; +import java.io.File; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The CosmosDB tests require the CosmosDB Emulator to be installed and running. + * + * More info at: https://aka.ms/documentdb-emulator-docs + * + * Also... Java requires the CosmosDB Emulator cert to be installed. See "Export the SSL certificate" in + * the link above to export the cert. Then import the cert into the Java JDK using: + * + * https://docs.microsoft.com/en-us/azure/java/java-sdk-add-certificate-ca-store?view=azure-java-stable + * + * Note: Don't ignore the first step of "At an administrator command prompt, navigate to your JDK's jdk\jre\lib\security folder" + */ +public class CosmosDbPartitionStorageTests extends StorageBaseTests { + private static boolean emulatorIsRunning = false; + private static final String NO_EMULATOR_MESSAGE = "This test requires CosmosDB Emulator! go to https://aka.ms/documentdb-emulator-docs to download and install."; + + private static String CosmosServiceEndpoint = "https://localhost:8081"; + private static String CosmosAuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + private static String CosmosDatabaseName = "test-db"; + private static String CosmosCollectionName = "bot-storage"; + + private Storage storage; + + @BeforeClass + public static void allTestsInit() throws IOException, InterruptedException, DocumentClientException { + File emulator = new File(System.getenv("ProgramFiles") + "\\Azure Cosmos DB Emulator\\CosmosDB.Emulator.exe"); + if (emulator.exists()) { + Process p = Runtime.getRuntime().exec + ("cmd /C \"" + emulator.getAbsolutePath() + " /GetStatus"); + + int result = p.waitFor(); + if (result == 2) { + emulatorIsRunning = true; + + DocumentClient client = new DocumentClient( + CosmosServiceEndpoint, + CosmosAuthKey, + ConnectionPolicy.GetDefault(), + ConsistencyLevel.Session + ); + + createDatabaseIfNeeded(client); + } + } + } + + @AfterClass + public static void allTestCleanup() throws DocumentClientException { + if (emulatorIsRunning) { + DocumentClient client = new DocumentClient( + CosmosServiceEndpoint, + CosmosAuthKey, + ConnectionPolicy.GetDefault(), + ConsistencyLevel.Session); + + List databaseList = client + .queryDatabases( + "SELECT * FROM root r WHERE r.id='" + CosmosDatabaseName + + "'", null).getQueryIterable().toList(); + if (databaseList.size() > 0) { + client.deleteDatabase(databaseList.get(0).getSelfLink(), null); + } + } + } + + @Before + public void testInit() { + if (emulatorIsRunning) { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey(CosmosAuthKey); + options.setContainerId(CosmosCollectionName); + options.setCosmosDbEndpoint(CosmosServiceEndpoint); + options.setDatabaseId(CosmosDatabaseName); + storage = new CosmosDbPartitionedStorage(options); + } + } + + @After + public void testCleanup() { + storage = null; + } + + @Test + public void constructorShouldThrowOnInvalidOptions() { + try { + new CosmosDbPartitionedStorage(null); + Assert.fail("should have thrown for null options"); + } catch(IllegalArgumentException e) { + // all good + } + + try { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey("test"); + options.setContainerId("testId"); + options.setDatabaseId("testDb"); + new CosmosDbPartitionedStorage(options); + Assert.fail("should have thrown for missing end point"); + } catch (IllegalArgumentException e) { + + } + + try { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey(null); + options.setContainerId("testId"); + options.setDatabaseId("testDb"); + options.setCosmosDbEndpoint("testEndpoint"); + new CosmosDbPartitionedStorage(options); + Assert.fail("should have thrown for missing auth key"); + } catch (IllegalArgumentException e) { + + } + + try { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey("testAuthKey"); + options.setContainerId("testId"); + options.setDatabaseId(null); + options.setCosmosDbEndpoint("testEndpoint"); + new CosmosDbPartitionedStorage(options); + Assert.fail("should have thrown for missing db id"); + } catch (IllegalArgumentException e) { + + } + + try { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey("testAuthKey"); + options.setContainerId(null); + options.setDatabaseId("testDb"); + options.setCosmosDbEndpoint("testEndpoint"); + new CosmosDbPartitionedStorage(options); + Assert.fail("should have thrown for missing collection id"); + } catch (IllegalArgumentException e) { + + } + + try { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey("testAuthKey"); + options.setContainerId("testId"); + options.setDatabaseId("testDb"); + options.setCosmosDbEndpoint("testEndpoint"); + options.setKeySuffix("?#*test"); + options.setCompatibilityMode(false); + new CosmosDbPartitionedStorage(options); + Assert.fail("should have thrown for invalid Row Key characters in KeySuffix"); + } catch (IllegalArgumentException e) { + + } + + try { + CosmosDbPartitionedStorageOptions options = new CosmosDbPartitionedStorageOptions(); + options.setAuthKey("testAuthKey"); + options.setContainerId("testId"); + options.setDatabaseId("testDb"); + options.setCosmosDbEndpoint("testEndpoint"); + options.setKeySuffix("thisisatest"); + options.setCompatibilityMode(true); + new CosmosDbPartitionedStorage(options); + Assert.fail("should have thrown for CompatibilityMode 'true' while using a KeySuffix"); + } catch (IllegalArgumentException e) { + + } + } + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + @Test + public void createObjectCosmosDBPartitionTest() { + if (runIfEmulator()) { + super.createObjectTest(storage); + } + } + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + @Test + public void readUnknownCosmosDBPartitionTest() { + if (runIfEmulator()) { + super.readUnknownTest(storage); + } + } + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + @Test + public void updateObjectCosmosDBPartitionTest() { + if (runIfEmulator()) { + super.updateObjectTest(storage); + } + } + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + @Test + public void deleteObjectCosmosDBPartitionTest() { + if (runIfEmulator()) { + super.deleteObjectTest(storage); + } + } + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + @Test + public void deleteUnknownObjectCosmosDBPartitionTest() { + if (runIfEmulator()) { + storage.delete(new String[]{"unknown_delete"}); + } + } + + // NOTE: THESE TESTS REQUIRE THAT THE COSMOS DB EMULATOR IS INSTALLED AND STARTED !!!!!!!!!!!!!!!!! + @Test + public void handleCrazyKeysCosmosDBPartition() { + if (runIfEmulator()) { + super.handleCrazyKeys(storage); + } + } + + @Test + public void readingEmptyKeysReturnsEmptyDictionary() { + if (runIfEmulator()) { + Map state = storage.read(new String[]{}).join(); + Assert.assertNotNull(state); + Assert.assertEquals(0, state.size()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void readingNullKeysThrowException() { + if (runIfEmulator()) { + storage.read(null).join(); + } else { + throw new IllegalArgumentException("bogus exception"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void writingNullStoreItemsThrowException() { + if (runIfEmulator()) { + storage.write(null); + } else { + throw new IllegalArgumentException("bogus exception"); + } + } + + @Test + public void writingNoStoreItemsDoesntThrow() { + if (runIfEmulator()) { + storage.write(new HashMap<>()); + } + } + + private static void createDatabaseIfNeeded(DocumentClient client) throws DocumentClientException { + // Get the database if it exists + List databaseList = client + .queryDatabases( + "SELECT * FROM root r WHERE r.id='" + CosmosDatabaseName + + "'", null).getQueryIterable().toList(); + + if (databaseList.size() == 0) { + // Create the database if it doesn't exist. + Database databaseDefinition = new Database(); + databaseDefinition.setId(CosmosDatabaseName); + + client.createDatabase( + databaseDefinition, null).getResource(); + } + } + + private boolean runIfEmulator() { + if (!emulatorIsRunning) { + System.out.println(NO_EMULATOR_MESSAGE); + return false; + } + + return true; + } +} diff --git a/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/TranscriptStoreTests.java b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/TranscriptStoreTests.java new file mode 100644 index 000000000..36acf2882 --- /dev/null +++ b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/TranscriptStoreTests.java @@ -0,0 +1,555 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure; + +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobContainerClientBuilder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.azure.blobs.BlobsTranscriptStore; +import com.microsoft.bot.builder.PagedResult; +import com.microsoft.bot.builder.TranscriptInfo; +import com.microsoft.bot.builder.TranscriptLoggerMiddleware; +import com.microsoft.bot.builder.TranscriptStore; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import org.apache.commons.lang3.StringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * These tests require Azure Storage Emulator v5.7 The emulator must be + * installed at this path C:\Program Files (x86)\Microsoft SDKs\Azure\Storage + * Emulator\AzureStorageEmulator.exe More info: + * https://docs.microsoft.com/azure/storage/common/storage-use-emulator + */ +public class TranscriptStoreTests { + + @Rule + public TestName TEST_NAME = new TestName(); + + protected String blobStorageEmulatorConnectionString = "AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"; + + private String channelId = "test"; + + private static final String[] CONVERSATION_IDS = { "qaz", "wsx", "edc", "rfv", "tgb", "yhn", "ujm", "123", "456", + "789", "ZAQ", "XSW", "CDE", "VFR", "BGT", "NHY", "NHY", "098", "765", "432", "zxc", "vbn", "mlk", "jhy", + "yui", "kly", "asd", "asw", "aaa", "zzz", }; + + private static final String[] CONVERSATION_SPECIAL_IDS = { "asd !&/#.'+:?\"", "ASD@123<>|}{][", "$%^;\\*()_" }; + + private String getContainerName() { + return String.format("blobstranscript%s", TEST_NAME.getMethodName().toLowerCase()); + } + + private TranscriptStore getTranscriptStore() { + return new BlobsTranscriptStore(blobStorageEmulatorConnectionString, getContainerName()); + } + + @Before + public void beforeTest() { + org.junit.Assume.assumeTrue(AzureEmulatorUtils.isStorageEmulatorAvailable()); + } + + @After + public void testCleanup() { + if (AzureEmulatorUtils.isStorageEmulatorAvailable()) { + BlobContainerClient containerClient = new BlobContainerClientBuilder() + .connectionString(blobStorageEmulatorConnectionString).containerName(getContainerName()) + .buildClient(); + + if (containerClient.exists()) { + containerClient.delete(); + } + } + } + + // These tests require Azure Storage Emulator v5.7 + @Test + public void blobTranscriptParamTest() { + PrintMethodName(); + Assert.assertThrows(IllegalArgumentException.class, () -> new BlobsTranscriptStore(null, getContainerName())); + Assert.assertThrows(IllegalArgumentException.class, + () -> new BlobsTranscriptStore(blobStorageEmulatorConnectionString, null)); + Assert.assertThrows(IllegalArgumentException.class, + () -> new BlobsTranscriptStore(new String(), getContainerName())); + Assert.assertThrows(IllegalArgumentException.class, + () -> new BlobsTranscriptStore(blobStorageEmulatorConnectionString, new String())); + } + + @Test + public void transcriptsEmptyTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + String unusedChannelId = UUID.randomUUID().toString(); + PagedResult transcripts = transcriptStore.listTranscripts(unusedChannelId).join(); + Assert.assertEquals(0, transcripts.getItems().size()); + } + + @Test + public void activityEmptyTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + for (String convoId : CONVERSATION_SPECIAL_IDS) { + PagedResult activities = transcriptStore.getTranscriptActivities(channelId, convoId).join(); + Assert.assertEquals(0, activities.getItems().size()); + } + } + + @Test + public void activityAddTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + Activity[] loggedActivities = new Activity[5]; + List activities = new ArrayList(); + for (int i = 0; i < 5; i++) { + Activity a = TranscriptStoreTests.createActivity(i, i, CONVERSATION_IDS); + transcriptStore.logActivity(a).join(); + activities.add(a); + loggedActivities[i] = transcriptStore.getTranscriptActivities(channelId, CONVERSATION_IDS[i]).join() + .getItems().get(0); + } + + Assert.assertEquals(5, loggedActivities.length); + } + + @Test + public void transcriptRemoveTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + for (int i = 0; i < 5; i++) { + Activity a = TranscriptStoreTests.createActivity(i, i, CONVERSATION_IDS); + transcriptStore.logActivity(a).join(); + transcriptStore.deleteTranscript(a.getChannelId(), a.getConversation().getId()).join(); + + PagedResult loggedActivities = transcriptStore + .getTranscriptActivities(channelId, CONVERSATION_IDS[i]).join(); + + Assert.assertEquals(0, loggedActivities.getItems().size()); + } + } + + @Test + public void activityAddSpecialCharsTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + Activity[] loggedActivities = new Activity[CONVERSATION_SPECIAL_IDS.length]; + List activities = new ArrayList(); + for (int i = 0; i < CONVERSATION_SPECIAL_IDS.length; i++) { + Activity a = TranscriptStoreTests.createActivity(i, i, CONVERSATION_SPECIAL_IDS); + transcriptStore.logActivity(a).join(); + activities.add(a); + int pos = i; + transcriptStore.getTranscriptActivities(channelId, CONVERSATION_SPECIAL_IDS[i]).thenAccept(result -> { + loggedActivities[pos] = result.getItems().get(0); + }); + } + + Assert.assertEquals(activities.size(), loggedActivities.length); + } + + @Test + public void transcriptRemoveSpecialCharsTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + for (int i = 0; i < CONVERSATION_SPECIAL_IDS.length; i++) { + Activity a = TranscriptStoreTests.createActivity(i, i, CONVERSATION_SPECIAL_IDS); + transcriptStore.deleteTranscript(a.getChannelId(), a.getConversation().getId()).join(); + + PagedResult loggedActivities = transcriptStore + .getTranscriptActivities(channelId, CONVERSATION_SPECIAL_IDS[i]).join(); + Assert.assertEquals(0, loggedActivities.getItems().size()); + } + } + + @Test + public void activityAddPagedResultTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + String cleanChannel = UUID.randomUUID().toString(); + + List activities = new ArrayList(); + + for (int i = 0; i < CONVERSATION_IDS.length; i++) { + Activity a = TranscriptStoreTests.createActivity(0, i, CONVERSATION_IDS); + a.setChannelId(cleanChannel); + + transcriptStore.logActivity(a).join(); + activities.add(a); + } + + PagedResult loggedPagedResult = transcriptStore + .getTranscriptActivities(cleanChannel, CONVERSATION_IDS[0]).join(); + String ct = loggedPagedResult.getContinuationToken(); + Assert.assertEquals(20, loggedPagedResult.getItems().size()); + Assert.assertNotNull(ct); + Assert.assertTrue(loggedPagedResult.getContinuationToken().length() > 0); + loggedPagedResult = transcriptStore.getTranscriptActivities(cleanChannel, CONVERSATION_IDS[0], ct).join(); + ct = loggedPagedResult.getContinuationToken(); + Assert.assertEquals(10, loggedPagedResult.getItems().size()); + Assert.assertNull(ct); + } + + @Test + public void transcriptRemovePagedTest() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + int i; + for (i = 0; i < CONVERSATION_SPECIAL_IDS.length; i++) { + Activity a = TranscriptStoreTests.createActivity(i, i, CONVERSATION_IDS); + transcriptStore.deleteTranscript(a.getChannelId(), a.getConversation().getId()).join(); + } + + PagedResult loggedActivities = transcriptStore.getTranscriptActivities(channelId, CONVERSATION_IDS[i]) + .join(); + Assert.assertEquals(0, loggedActivities.getItems().size()); + } + + @Test + public void nullParameterTests() { + PrintMethodName(); + TranscriptStore store = getTranscriptStore(); + + Assert.assertThrows(IllegalArgumentException.class, () -> store.logActivity(null)); + Assert.assertThrows(IllegalArgumentException.class, + () -> store.getTranscriptActivities(null, CONVERSATION_IDS[0])); + Assert.assertThrows(IllegalArgumentException.class, () -> store.getTranscriptActivities(channelId, null)); + } + + @Test + public void logActivities() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + ConversationReference conversation = TestAdapter.createConversationReference(UUID.randomUUID().toString(), + "User1", "Bot"); + TestAdapter adapter = new TestAdapter(conversation).use(new TranscriptLoggerMiddleware(transcriptStore)); + new TestFlow(adapter, turnContext -> { + delay(500); + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + turnContext.sendActivity(typingActivity).join(); + delay(500); + turnContext.sendActivity(String.format("echo:%s", turnContext.getActivity().getText())).join(); + return CompletableFuture.completedFuture(null); + }).send("foo").assertReply(activity -> Assert.assertTrue(activity.isType(ActivityTypes.TYPING))) + .assertReply("echo:foo").send("bar") + .assertReply(activity -> Assert.assertTrue(activity.isType(ActivityTypes.TYPING))) + .assertReply("echo:bar").startTest().join(); + + PagedResult pagedResult = null; + try { + pagedResult = this.getPagedResult(conversation, 6, null).join(); + } catch (TimeoutException ex) { + Assert.fail(); + } + Assert.assertEquals(6, pagedResult.getItems().size()); + Assert.assertTrue(pagedResult.getItems().get(0).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("foo", pagedResult.getItems().get(0).getText()); + Assert.assertNotNull(pagedResult.getItems().get(1)); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.TYPING)); + Assert.assertTrue(pagedResult.getItems().get(2).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("echo:foo", pagedResult.getItems().get(2).getText()); + Assert.assertTrue(pagedResult.getItems().get(3).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("bar", pagedResult.getItems().get(3).getText()); + Assert.assertNotNull(pagedResult.getItems().get(4)); + Assert.assertTrue(pagedResult.getItems().get(4).isType(ActivityTypes.TYPING)); + Assert.assertTrue(pagedResult.getItems().get(5).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("echo:bar", pagedResult.getItems().get(5).getText()); + for (Activity activity : pagedResult.getItems()) { + Assert.assertTrue(!StringUtils.isBlank(activity.getId())); + Assert.assertTrue(activity.getTimestamp().isAfter(OffsetDateTime.MIN)); + } + } + + @Test + public void logUpdateActivities() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + ConversationReference conversation = TestAdapter.createConversationReference(UUID.randomUUID().toString(), + "User1", "Bot"); + TestAdapter adapter = new TestAdapter(conversation).use(new TranscriptLoggerMiddleware(transcriptStore)); + final Activity[] activityToUpdate = { null }; + new TestFlow(adapter, turnContext -> { + delay(500); + if (turnContext.getActivity().getText().equals("update")) { + activityToUpdate[0].setText("new response"); + turnContext.updateActivity(activityToUpdate[0]).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + ResourceResponse response = turnContext.sendActivity(activity).join(); + activity.setId(response.getId()); + + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + try { + // clone the activity, so we can use it to do an update + activityToUpdate[0] = objectMapper.readValue(objectMapper.writeValueAsString(activity), + Activity.class); + } catch (JsonProcessingException ex) { + ex.printStackTrace(); + } + } + return CompletableFuture.completedFuture(null); + }).send("foo").send("update").assertReply("new response").startTest().join(); + + PagedResult pagedResult = null; + try { + pagedResult = this.getPagedResult(conversation, 3, null).join(); + } catch (TimeoutException ex) { + Assert.fail(); + } + + Assert.assertEquals(3, pagedResult.getItems().size()); + Assert.assertTrue(pagedResult.getItems().get(0).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("foo", pagedResult.getItems().get(0).getText()); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("new response", pagedResult.getItems().get(1).getText()); + Assert.assertTrue(pagedResult.getItems().get(2).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("update", pagedResult.getItems().get(2).getText()); + } + + @Test + public void logMissingUpdateActivity() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + ConversationReference conversation = TestAdapter.createConversationReference(UUID.randomUUID().toString(), + "User1", "Bot"); + TestAdapter adapter = new TestAdapter(conversation).use(new TranscriptLoggerMiddleware(transcriptStore)); + final String[] fooId = { new String() }; + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + new TestFlow(adapter, turnContext -> { + fooId[0] = turnContext.getActivity().getId(); + Activity updateActivity = null; + try { + // clone the activity, so we can use it to do an update + updateActivity = objectMapper.readValue(objectMapper.writeValueAsString(turnContext.getActivity()), + Activity.class); + } catch (JsonProcessingException ex) { + ex.printStackTrace(); + } + updateActivity.setText("updated response"); + ResourceResponse response = turnContext.updateActivity(updateActivity).join(); + return CompletableFuture.completedFuture(null); + }).send("foo").startTest().join(); + + delay(3000); + + PagedResult pagedResult = null; + try { + pagedResult = this.getPagedResult(conversation, 2, null).join(); + } catch (TimeoutException ex) { + Assert.fail(); + } + + Assert.assertEquals(2, pagedResult.getItems().size()); + Assert.assertTrue(pagedResult.getItems().get(0).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals(fooId[0], pagedResult.getItems().get(0).getId()); + Assert.assertEquals("foo", pagedResult.getItems().get(0).getText()); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.MESSAGE)); + Assert.assertTrue(pagedResult.getItems().get(1).getId().startsWith("g_")); + Assert.assertEquals("updated response", pagedResult.getItems().get(1).getText()); + } + + @Test + public void testDateLogUpdateActivities() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + OffsetDateTime dateTimeStartOffset1 = OffsetDateTime.now(); + ConversationReference conversation = TestAdapter.createConversationReference(UUID.randomUUID().toString(), + "User1", "Bot"); + TestAdapter adapter = new TestAdapter(conversation).use(new TranscriptLoggerMiddleware(transcriptStore)); + final Activity[] activityToUpdate = { null }; + new TestFlow(adapter, turnContext -> { + if (turnContext.getActivity().getText().equals("update")) { + activityToUpdate[0].setText("new response"); + turnContext.updateActivity(activityToUpdate[0]).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + + ResourceResponse response = turnContext.sendActivity(activity).join(); + activity.setId(response.getId()); + + delay(200); + + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + try { + // clone the activity, so we can use it to do an update + activityToUpdate[0] = objectMapper.readValue(objectMapper.writeValueAsString(activity), + Activity.class); + } catch (JsonProcessingException ex) { + ex.printStackTrace(); + } + } + return CompletableFuture.completedFuture(null); + }).send("foo").delay(500).send("update").delay(500).assertReply("new response").startTest().join(); + + try { + TimeUnit.MILLISECONDS.sleep(5000); + } catch (InterruptedException e) { + // Empty error + } + + // Perform some queries + PagedResult pagedResult = transcriptStore.getTranscriptActivities(conversation.getChannelId(), + conversation.getConversation().getId(), null, dateTimeStartOffset1).join(); + Assert.assertEquals(3, pagedResult.getItems().size()); + Assert.assertTrue(pagedResult.getItems().get(0).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("foo", pagedResult.getItems().get(0).getText()); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("new response", pagedResult.getItems().get(1).getText()); + Assert.assertTrue(pagedResult.getItems().get(2).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("update", pagedResult.getItems().get(2).getText()); + + // Perform some queries + pagedResult = transcriptStore.getTranscriptActivities(conversation.getChannelId(), + conversation.getConversation().getId(), null, OffsetDateTime.MIN).join(); + Assert.assertEquals(3, pagedResult.getItems().size()); + Assert.assertTrue(pagedResult.getItems().get(0).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("foo", pagedResult.getItems().get(0).getText()); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("new response", pagedResult.getItems().get(1).getText()); + Assert.assertTrue(pagedResult.getItems().get(2).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("update", pagedResult.getItems().get(2).getText()); + + // Perform some queries + pagedResult = transcriptStore.getTranscriptActivities(conversation.getChannelId(), + conversation.getConversation().getId(), null, OffsetDateTime.MAX).join(); + Assert.assertEquals(0, pagedResult.getItems().size()); + } + + @Test + public void logDeleteActivities() { + PrintMethodName(); + TranscriptStore transcriptStore = getTranscriptStore(); + + ConversationReference conversation = TestAdapter.createConversationReference(UUID.randomUUID().toString(), + "User1", "Bot"); + TestAdapter adapter = new TestAdapter(conversation).use(new TranscriptLoggerMiddleware(transcriptStore)); + final String[] activityId = { null }; + new TestFlow(adapter, turnContext -> { + delay(500); + if (turnContext.getActivity().getText().equals("deleteIt")) { + turnContext.deleteActivity(activityId[0]).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + ResourceResponse response = turnContext.sendActivity(activity).join(); + activityId[0] = response.getId(); + } + return CompletableFuture.completedFuture(null); + }).send("foo").assertReply("response").send("deleteIt").startTest().join(); + + PagedResult pagedResult = null; + try { + pagedResult = this.getPagedResult(conversation, 3, null).join(); + } catch (TimeoutException ex) { + Assert.fail(); + } + + Assert.assertEquals(3, pagedResult.getItems().size()); + Assert.assertTrue(pagedResult.getItems().get(0).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("foo", pagedResult.getItems().get(0).getText()); + Assert.assertNotNull(pagedResult.getItems().get(1)); + Assert.assertTrue(pagedResult.getItems().get(1).isType(ActivityTypes.MESSAGE_DELETE)); + Assert.assertTrue(pagedResult.getItems().get(2).isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("deleteIt", pagedResult.getItems().get(2).getText()); + } + + protected static Activity createActivity(Integer i, Integer j, String[] CONVERSATION_IDS) { + return TranscriptStoreTests.createActivity(j, CONVERSATION_IDS[i]); + } + + private static Activity createActivity(Integer j, String conversationId) { + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId(conversationId); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setId(StringUtils.leftPad(String.valueOf(j + 1), 2, "0")); + activity.setChannelId("test"); + activity.setText("test"); + activity.setConversation(conversationAccount); + activity.setTimestamp(OffsetDateTime.now()); + activity.setFrom(new ChannelAccount("testUser")); + activity.setRecipient(new ChannelAccount("testBot")); + return activity; + } + + /** + * There are some async oddities within TranscriptLoggerMiddleware that make it + * difficult to set a short delay when running this tests that ensures the + * TestFlow completes while also logging transcripts. Some tests will not pass + * without longer delays, but this method minimizes the delay required. + * + * @param conversation ConversationReference to pass to + * GetTranscriptActivitiesAsync() that contains ChannelId + * and Conversation.Id. + * @param expectedLength Expected length of pagedResult array. + * @param maxTimeout Maximum time to wait to retrieve pagedResult. + * @return PagedResult. + * @throws TimeoutException + */ + private CompletableFuture> getPagedResult(ConversationReference conversation, + Integer expectedLength, Integer maxTimeout) throws TimeoutException { + TranscriptStore transcriptStore = getTranscriptStore(); + if (maxTimeout == null) { + maxTimeout = 10000; + } + + PagedResult pagedResult = null; + for (int timeout = 0; timeout < maxTimeout; timeout += 500) { + delay(500); + try { + pagedResult = transcriptStore + .getTranscriptActivities(conversation.getChannelId(), conversation.getConversation().getId()) + .join(); + if (pagedResult.getItems().size() >= expectedLength) { + break; + } + } catch (NoSuchElementException ex) { + } catch (NullPointerException e) { + } + } + + if (pagedResult == null) { + return Async.completeExceptionally(new TimeoutException("Unable to retrieve pagedResult in time")); + } + + return CompletableFuture.completedFuture(pagedResult); + } + + private void PrintMethodName() + { + System.out.println("Running " + (new Throwable().getStackTrace()[1].getMethodName()) + "()"); + } + + /** + * Time period delay. + * + * @param delay Time to delay. + */ + private void delay(Integer delay) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // Empty error + } + } +} diff --git a/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/blobs/BlobsStorageTests.java b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/blobs/BlobsStorageTests.java new file mode 100644 index 000000000..3197972bc --- /dev/null +++ b/libraries/bot-azure/src/test/java/com/microsoft/bot/azure/blobs/BlobsStorageTests.java @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.azure.blobs; + +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobContainerClientBuilder; +import com.microsoft.bot.azure.AzureEmulatorUtils; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.StorageBaseTests; +import com.microsoft.bot.builder.StoreItem; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class BlobsStorageTests extends StorageBaseTests { + + @Rule + public TestName testName = new TestName(); + + private final String connectionString = "AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"; + + public String getContainerName() { + return "blobs" + testName.getMethodName().toLowerCase().replace("_", ""); + } + + @Before + public void beforeTest() { + org.junit.Assume.assumeTrue(AzureEmulatorUtils.isStorageEmulatorAvailable()); + } + + @After + public void testCleanup() { + if (AzureEmulatorUtils.isStorageEmulatorAvailable()) { + BlobContainerClient containerClient = new BlobContainerClientBuilder().connectionString(connectionString) + .containerName(getContainerName()).buildClient(); + + if (containerClient.exists()) { + containerClient.delete(); + } + } + } + + @Test + public void blobStorageParamTest() { + Assert.assertThrows(IllegalArgumentException.class, () -> new BlobsStorage(null, getContainerName())); + Assert.assertThrows(IllegalArgumentException.class, () -> new BlobsStorage(connectionString, null)); + Assert.assertThrows(IllegalArgumentException.class, () -> new BlobsStorage(new String(), getContainerName())); + Assert.assertThrows(IllegalArgumentException.class, () -> new BlobsStorage(connectionString, new String())); + } + + @Test + public void testBlobStorageWriteRead() { + // Arrange + Storage storage = new BlobsStorage(connectionString, getContainerName()); + + Map changes = new HashMap(); + changes.put("x", "hello"); + changes.put("y", "world"); + + // Act + storage.write(changes).join(); + Map result = storage.read(new String[] { "x", "y" }).join(); + + // Assert + Assert.assertEquals(2, result.size()); + Assert.assertEquals("hello", result.get("x")); + Assert.assertEquals("world", result.get("y")); + } + + @Test + public void testBlobStorageWriteDeleteRead() { + // Arrange + Storage storage = new BlobsStorage(connectionString, getContainerName()); + + Map changes = new HashMap(); + changes.put("x", "hello"); + changes.put("y", "world"); + + // Act + storage.write(changes).join(); + storage.delete(new String[] { "x" }).join(); + Map result = storage.read(new String[] { "x", "y" }).join(); + + // Assert + Assert.assertEquals(1, result.size()); + Assert.assertEquals("world", result.get("y")); + } + + @Test + public void testBlobStorageChanges() { + // Arrange + Storage storage = new BlobsStorage(connectionString, getContainerName()); + + // Act + Map changes = new HashMap(); + changes.put("a", "1.0"); + changes.put("b", "2.0"); + storage.write(changes).join(); + + changes.clear(); + changes.put("c", "3.0"); + storage.write(changes).join(); + storage.delete(new String[] { "b" }).join(); + + changes.clear(); + changes.put("a", "1.1"); + storage.write(changes).join(); + + Map result = storage.read(new String[] { "a", "b", "c", "d", "e" }).join(); + + // Assert + Assert.assertEquals(2, result.size()); + Assert.assertEquals("1.1", result.get("a")); + Assert.assertEquals("3.0", result.get("c")); + } + + @Test + public void testConversationStateBlobStorage() { + // Arrange + Storage storage = new BlobsStorage(connectionString, getContainerName()); + + ConversationState conversationState = new ConversationState(storage); + StatePropertyAccessor propAccessor = conversationState.createProperty("prop"); + + TestStorageAdapter adapter = new TestStorageAdapter(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId("123"); + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId("abc"); + activity.setConversation(conversationAccount); + + // Act + TurnContext turnContext1 = new TurnContextImpl(adapter, activity); + Prop propValue1 = propAccessor.get(turnContext1, Prop::new).join(); + propValue1.setX("hello"); + propValue1.setY("world"); + conversationState.saveChanges(turnContext1).join(); + + TurnContext turnContext2 = new TurnContextImpl(adapter, activity); + Prop propValue2 = propAccessor.get(turnContext2).join(); + + // Assert + Assert.assertEquals("hello", propValue2.getX()); + Assert.assertEquals("world", propValue2.getY()); + + propAccessor.delete(turnContext1).join(); + conversationState.saveChanges(turnContext1).join(); + } + + @Test + public void testConversationStateBlobStorage_TypeNameHandlingDefault() { + Storage storage = new BlobsStorage(connectionString, getContainerName()); + testConversationStateBlobStorage_Method(storage); + } + + @Test + public void statePersistsThroughMultiTurn_TypeNameHandlingNone() { + Storage storage = new BlobsStorage(connectionString, getContainerName()); + statePersistsThroughMultiTurn(storage); + } + + private void testConversationStateBlobStorage_Method(Storage blobs) { + // Arrange + ConversationState conversationState = new ConversationState(blobs); + StatePropertyAccessor propAccessor = conversationState.createProperty("prop"); + TestStorageAdapter adapter = new TestStorageAdapter(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId("123"); + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId("abc"); + activity.setConversation(conversationAccount); + + // Act + TurnContext turnContext1 = new TurnContextImpl(adapter, activity); + Prop propValue1 = propAccessor.get(turnContext1, Prop::new).join(); + propValue1.setX("hello"); + propValue1.setY("world"); + conversationState.saveChanges(turnContext1).join(); + + TurnContext turnContext2 = new TurnContextImpl(adapter, activity); + Prop propValue2 = propAccessor.get(turnContext2).join(); + + // Assert + Assert.assertEquals("hello", propValue2.getX()); + Assert.assertEquals("world", propValue2.getY()); + } + + private class TestStorageAdapter extends BotAdapter { + + @Override + public CompletableFuture sendActivities(TurnContext context, List activities) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture updateActivity(TurnContext context, Activity activity) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture deleteActivity(TurnContext context, ConversationReference reference) { + throw new UnsupportedOperationException(); + } + } + + private static class Prop { + private String X; + private String Y; + StoreItem storeItem; + + public String getX() { + return X; + } + + public void setX(String x) { + X = x; + } + + public String getY() { + return Y; + } + + public void setY(String y) { + Y = y; + } + + public StoreItem getStoreItem() { + return storeItem; + } + + public void setStoreItem(StoreItem storeItem) { + this.storeItem = storeItem; + } + } +} diff --git a/libraries/bot-builder/README.md b/libraries/bot-builder/README.md new file mode 100644 index 000000000..29f7ebd64 --- /dev/null +++ b/libraries/bot-builder/README.md @@ -0,0 +1,14 @@ + +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/libraries/bot-builder/pom.xml b/libraries/bot-builder/pom.xml new file mode 100644 index 000000000..bdf4df4f6 --- /dev/null +++ b/libraries/bot-builder/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-builder + jar + + ${project.groupId}:${project.artifactId} + Bot Framework Builder + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + + org.slf4j + slf4j-api + + + org.mockito + mockito-core + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.codepoetics + protonpack + + + com.auth0 + java-jwt + + + com.auth0 + jwks-rsa + + + + com.microsoft.bot + bot-schema + + + com.microsoft.bot + bot-connector + + + + + + build + + true + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.1 + + + + test-jar + + + + + + + + + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java new file mode 100644 index 000000000..0a4fe8d25 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java @@ -0,0 +1,781 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Async; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.AdaptiveCardInvokeResponse; +import com.microsoft.bot.schema.AdaptiveCardInvokeValue; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.MessageReaction; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.SignInConstants; + +/** + * An implementation of the {@link Bot} interface intended for further + * subclassing. Derive from this class to plug in code to handle particular + * {@link Activity} types. Pre and post processing of Activities can be plugged + * in by deriving and calling the base class implementation. + */ +@Deprecated +public class ActivityHandler implements Bot { + + /** + * Called by the adapter (for example, a {@link BotFrameworkAdapter}) at runtime + * in order to process an inbound {@link Activity}. + * + *

+ * This method calls other methods in this class based on the type of the + * activity to process, which allows a derived class to provide type-specific + * logic in a controlled way. + *

+ * + *

+ * In a derived class, override this method to add logic that applies to all + * activity types. Add logic to apply before the type-specific logic before the + * call to the base class {@link Bot#onTurn(TurnContext)} method. Add logic to + * apply after the type-specific logic after the call to the base class + * {@link Bot#onTurn(TurnContext)} method. + *

+ * + * @param turnContext The context object for this turn. Provides information + * about the incoming activity, and other data needed to + * process the activity. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "TurnContext cannot be null." + )); + } + + if (turnContext.getActivity() == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext must have a non-null Activity." + )); + } + + if (turnContext.getActivity().getType() == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext.getActivity must have a non-null Type." + )); + } + + switch (turnContext.getActivity().getType()) { + case ActivityTypes.MESSAGE: + return onMessageActivity(turnContext); + + case ActivityTypes.CONVERSATION_UPDATE: + return onConversationUpdateActivity(turnContext); + + case ActivityTypes.MESSAGE_REACTION: + return onMessageReactionActivity(turnContext); + + case ActivityTypes.EVENT: + return onEventActivity(turnContext); + + case ActivityTypes.INSTALLATION_UPDATE: + return onInstallationUpdate(turnContext); + + case ActivityTypes.COMMAND: + return onCommandActivity(turnContext); + + case ActivityTypes.COMMAND_RESULT: + return onCommandResultActivity(turnContext); + + case ActivityTypes.END_OF_CONVERSATION: + return onEndOfConversationActivity(turnContext); + + case ActivityTypes.TYPING: + return onTypingActivity(turnContext); + + case ActivityTypes.INVOKE: + return onInvokeActivity(turnContext).thenCompose(invokeResponse -> { + // If OnInvokeActivityAsync has already sent an InvokeResponse, do not send + // another one. + if ( + invokeResponse != null && turnContext.getTurnState() + .get(BotFrameworkAdapter.INVOKE_RESPONSE_KEY) == null + ) { + + Activity activity = new Activity(ActivityTypes.INVOKE_RESPONSE); + activity.setValue(invokeResponse); + + return turnContext.sendActivity(activity); + } + + CompletableFuture noAction = new CompletableFuture<>(); + noAction.complete(null); + return noAction; + }).thenApply(response -> null); + + default: + return onUnrecognizedActivityType(turnContext); + } + } + + /** + * Override this in a derived class to provide logic specific to + * {@link ActivityTypes#MESSAGE} activities, such as the conversational logic. + *

+ * When the {@link #onTurn(TurnContext)} method receives a message activity, it + * calls this method. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a conversation update activity is received from the channel when + * the base behavior of {@link #onTurn(TurnContext)} is used. + *

+ * Conversation update activities are useful when it comes to responding to + * users being added to or removed from the conversation. + *

+ * For example, a bot could respond to a user being added by greeting the user. + * By default, this method will call {@link #onMembersAdded(List, TurnContext)} + * if any users have been added or {@link #onMembersRemoved(List, TurnContext)} + * if any users have been removed. The method checks the member ID so that it + * only responds to updates regarding members other than the bot itself. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onConversationUpdateActivity(TurnContext turnContext) { + Activity activity = turnContext.getActivity(); + + if ( + activity.getMembersAdded() != null && activity.getRecipient() != null + && activity.getMembersAdded() + .stream() + .anyMatch(m -> !StringUtils.equals(m.getId(), activity.getRecipient().getId())) + ) { + return onMembersAdded(activity.getMembersAdded(), turnContext); + } else if ( + activity.getMembersRemoved() != null && activity.getRecipient() != null + && activity.getMembersRemoved() + .stream() + .anyMatch(m -> !StringUtils.equals(m.getId(), activity.getRecipient().getId())) + ) { + return onMembersRemoved(activity.getMembersRemoved(), turnContext); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic for when members other than + * the bot join the conversation, such as your bot's welcome logic. + * + *

+ * When the {@link #onConversationUpdateActivity(TurnContext)} method receives a + * conversation update activity that indicates one or more users other than the + * bo are joining the conversation, it calls this method. + *

+ * + * @param membersAdded A list of all the members added to the conversation, as + * described by the conversation update activity. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onMembersAdded( + List membersAdded, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic for when members other than + * the bot leave the conversation, such as your bot's good-bye logic. + * + *

+ * When the {@link #onConversationUpdateActivity(TurnContext)} method receives a + * conversation update activity that indicates one or more users other than the + * bot are leaving the conversation, it calls this method. + *

+ * + * @param membersRemoved A list of all the members removed from the + * conversation, as described by the conversation update + * activity. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onMembersRemoved( + List membersRemoved, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when an event activity is received from the connector when the base + * behavior of {@link #onTurn(TurnContext)} is used. + * + *

+ * Message reactions correspond to the user adding a 'like' or 'sad' etc. (often + * an emoji) to a previously sent activity. Message reactions are only supported + * by a few channels. + *

+ * + *

+ * The activity that the message reaction corresponds to is indicated in the + * replyToId property. The value of this property is the activity id of a + * previously sent activity given back to the bot as the response from a send + * call. + *

+ * + *

+ * When the {@link #onTurn(TurnContext)} method receives a message reaction + * activity, it calls this method. If the message reaction indicates that + * reactions were added to a message, it calls + * {@link #onReactionsAdded(List, TurnContext)}. If the message reaction + * indicates that reactions were removed from a message, it calls + * {@link #onReactionsRemoved(List, TurnContext)}. + *

+ * + *

+ * In a derived class, override this method to add logic that applies to all + * message reaction activities. Add logic to apply before the reactions added or + * removed logic before the call to the base class method. Add logic to apply + * after the reactions added or removed logic after the call to the base class. + *

+ * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onMessageReactionActivity(TurnContext turnContext) { + CompletableFuture task = null; + + if (turnContext.getActivity().getReactionsAdded() != null) { + task = onReactionsAdded(turnContext.getActivity().getReactionsAdded(), turnContext); + } + + if (turnContext.getActivity().getReactionsRemoved() != null) { + if (task != null) { + task.thenApply( + result -> onReactionsRemoved( + turnContext.getActivity().getReactionsRemoved(), turnContext + ) + ); + } else { + task = onReactionsRemoved( + turnContext.getActivity().getReactionsRemoved(), turnContext + ); + } + } + + return task == null ? CompletableFuture.completedFuture(null) : task; + } + + /** + * Override this in a derived class to provide logic for when reactions to a + * previous activity are added to the conversation. + * + *

+ * Message reactions correspond to the user adding a 'like' or 'sad' etc. (often + * an emoji) to a previously sent message on the conversation. Message reactions + * are supported by only a few channels. The activity that the message is in + * reaction to is identified by the activity's {@link Activity#getReplyToId()} + * property. The value of this property is the activity ID of a previously sent + * activity. When the bot sends an activity, the channel assigns an ID to it, + * which is available in the + * {@link com.microsoft.bot.schema.ResourceResponse#getId} of the result. + *

+ * + * @param messageReactions The list of reactions added. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onReactionsAdded( + List messageReactions, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic for when reactions to a + * previous activity are removed from the conversation. + * + *

+ * Message reactions correspond to the user adding a 'like' or 'sad' etc. (often + * an emoji) to a previously sent message on the conversation. Message reactions + * are supported by only a few channels. The activity that the message is in + * reaction to is identified by the activity's {@link Activity#getReplyToId()} + * property. The value of this property is the activity ID of a previously sent + * activity. When the bot sends an activity, the channel assigns an ID to it, + * which is available in the + * {@link com.microsoft.bot.schema.ResourceResponse#getId} of the result. + *

+ * + * @param messageReactions The list of reactions removed. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onReactionsRemoved( + List messageReactions, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when an event activity is received from the connector when the base + * behavior of {@link #onTurn(TurnContext)} is used. + * + *

+ * Event activities can be used to communicate many different things. + *

+ * + *

+ * By default, this method will call {@link #onTokenResponseEvent(TurnContext)} + * if the activity's name is "tokens/response" or {@link #onEvent(TurnContext)} + * otherwise. "tokens/response" event can be triggered by an + * {@link com.microsoft.bot.schema.OAuthCard}. + *

+ * + *

+ * When the {@link #onTurn(TurnContext)} method receives an event activity, it + * calls this method. + *

+ * + *

+ * If the event {@link Activity#getName} is `tokens/response`, it calls + * {@link #onTokenResponseEvent(TurnContext)} otherwise, it calls + * {@link #onEvent(TurnContext)}. + *

+ * + *

+ * In a derived class, override this method to add logic that applies to all + * event activities. Add logic to apply before the specific event-handling logic + * before the call to the base class method. Add logic to apply after the + * specific event-handling logic after the call to the base class method. + *

+ * + *

+ * Event activities communicate programmatic information from a client or + * channel to a bot. The meaning of an event activity is defined by the + * {@link Activity#getName} property, which is meaningful within the scope of a + * channel. A `tokens/response` event can be triggered by an + * {@link com.microsoft.bot.schema.OAuthCard} or an OAuth prompt. + *

+ * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onEventActivity(TurnContext turnContext) { + if (StringUtils.equals(turnContext.getActivity().getName(), "tokens/response")) { + return onTokenResponseEvent(turnContext); + } + + return onEvent(turnContext); + } + + /** + * Invoked when an invoke activity is received from the connector when the base + * behavior of onTurn is used. + *

+ * Invoke activities can be used to communicate many different things. By + * default, this method will call onSignInInvokeAsync if the activity's name is + * 'signin/verifyState' or 'signin/tokenExchange'. + *

+ * A 'signin/verifyState' or 'signin/tokenExchange' invoke can be triggered by + * an OAuthCard. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onInvokeActivity(TurnContext turnContext) { + if (StringUtils.equals(turnContext.getActivity().getName(), "adaptiveCard/action")) { + AdaptiveCardInvokeValue invokeValue = null; + try { + invokeValue = getAdaptiveCardInvokeValue(turnContext.getActivity()); + } catch (InvokeResponseException e) { + return Async.completeExceptionally(e); + } + return onAdaptiveCardInvoke(turnContext, invokeValue).thenApply(result -> createInvokeResponse(result)); + } + + if ( + StringUtils.equals( + turnContext.getActivity().getName(), SignInConstants.VERIFY_STATE_OPERATION_NAME + ) || StringUtils.equals( + turnContext.getActivity().getName(), SignInConstants.TOKEN_EXCHANGE_OPERATION_NAME + ) + ) { + return onSignInInvoke(turnContext).thenApply(aVoid -> createInvokeResponse(null)) + .exceptionally(ex -> { + if ( + ex instanceof CompletionException + && ex.getCause() instanceof InvokeResponseException + ) { + InvokeResponseException ire = (InvokeResponseException) ex.getCause(); + return new InvokeResponse(ire.statusCode, ire.body); + } else if (ex instanceof InvokeResponseException) { + InvokeResponseException ire = (InvokeResponseException) ex; + return new InvokeResponse(ire.statusCode, ire.body); + } + return new InvokeResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null); + }); + } + + CompletableFuture result = new CompletableFuture<>(); + result.complete(new InvokeResponse(HttpURLConnection.HTTP_NOT_IMPLEMENTED, null)); + return result; + } + + /** + * Invoked when a 'signin/verifyState' or 'signin/tokenExchange' event is + * received when the base behavior of onInvokeActivity is used. + *

+ * If using an OAuthPrompt, override this method to forward this Activity to the + * current dialog. By default, this method does nothing. + *

+ * When the onInvokeActivity method receives an Invoke with a name of + * `tokens/response`, it calls this method. + *

+ * If your bot uses the OAuthPrompt, forward the incoming Activity to the + * current dialog. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onSignInInvoke(TurnContext turnContext) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally( + new InvokeResponseException(HttpURLConnection.HTTP_NOT_IMPLEMENTED) + ); + return result; + } + + /** + * Creates a success InvokeResponse with the specified body. + * + * @param body The body to return in the invoke response. + * @return The InvokeResponse object. + */ + protected InvokeResponse createInvokeResponse(Object body) { + return new InvokeResponse(HttpURLConnection.HTTP_OK, body); + } + + /** + * Invoked when a "tokens/response" event is received when the base behavior of + * {@link #onEventActivity(TurnContext)} is used. + * + *

+ * If using an OAuthPrompt, override this method to forward this + * {@link Activity} to the current dialog. + *

+ * + *

+ * By default, this method does nothing. + *

+ *

+ * When the {@link #onEventActivity(TurnContext)} method receives an event with + * a {@link Activity#getName()} of `tokens/response`, it calls this method. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTokenResponseEvent(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when an event other than tokens/response is received when the base + * behavior of {@link #onEventActivity(TurnContext)} is used. + * + *

+ * This method could optionally be overridden if the bot is meant to handle + * miscellaneous events. + *

+ * + *

+ * By default, this method does nothing. + *

+ *

+ * When the {@link #onEventActivity(TurnContext)} method receives an event with + * a {@link Activity#getName()} other than `tokens/response`, it calls this + * method. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onEvent(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic specific to + * ActivityTypes.InstallationUpdate activities. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onInstallationUpdate(TurnContext turnContext) { + String action = turnContext.getActivity().getAction(); + if (StringUtils.isEmpty(action)) { + return CompletableFuture.completedFuture(null); + } + + switch (action) { + case "add": + case "add-upgrade": + return onInstallationUpdateAdd(turnContext); + + case "remove": + case "remove-upgrade": + return onInstallationUpdateRemove(turnContext); + + default: + return CompletableFuture.completedFuture(null); + } + } + + /** + * Invoked when a command activity is received when the base behavior of + * {@link ActivityHandler#onTurn(TurnContext)} is used. Commands are requests to perform an + * action and receivers typically respond with one or more commandResult + * activities. Receivers are also expected to explicitly reject unsupported + * command activities. + * + * @param turnContext A strongly-typed context Object for this + * turn. + * + * @return A task that represents the work queued to execute. + * + * When the {@link ActivityHandler#onTurn(TurnContext)} method receives a command activity, + * it calls this method. In a derived class, override this method to add + * logic that applies to all comand activities. Add logic to apply before + * the specific command-handling logic before the call to the base class + * {@link ActivityHandler#onCommandActivity(TurnContext)} method. Add + * logic to apply after the specific command-handling logic after the call + * to the base class + * {@link ActivityHandler#onCommandActivity(TurnContext)} method. Command + * activities communicate programmatic information from a client or channel + * to a bot. The meaning of an command activity is defined by the + * name property, which is meaningful within the scope of a channel. + */ + protected CompletableFuture onCommandActivity(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a CommandResult activity is received when the + * base behavior of {@link ActivityHandler#onTurn(TurnContext)} is used. CommandResult + * activities can be used to communicate the result of a command execution. + * + * @param turnContext A strongly-typed context Object for this + * turn. + * + * @return A task that represents the work queued to execute. + * + * When the {@link ActivityHandler#onTurn(TurnContext)} method receives a CommandResult + * activity, it calls this method. In a derived class, override this method + * to add logic that applies to all comand activities. Add logic to apply + * before the specific CommandResult-handling logic before the call to the + * base class + * {@link ActivityHandler#onCommandResultActivity(TurnContext)} + * method. Add logic to apply after the specific CommandResult-handling + * logic after the call to the base class + * {@link ActivityHandler#onCommandResultActivity(TurnContext)} + * method. CommandResult activities communicate programmatic information + * from a client or channel to a bot. The meaning of an CommandResult + * activity is defined by the name property, + * which is meaningful within the scope of a channel. + */ + protected CompletableFuture onCommandResultActivity(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic specific to ActivityTypes.InstallationUpdate + * activities with 'action' set to 'add'. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onInstallationUpdateAdd(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic specific to ActivityTypes.InstallationUpdate + * activities with 'action' set to 'remove'. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onInstallationUpdateRemove(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when the bot is sent an Adaptive Card Action Execute. + * + * @param turnContext A strongly-typed context Object for this + * turn. + * @param invokeValue A stringly-typed Object from the incoming + * activity's Value. + * + * @return A task that represents the work queued to execute. + * + * When the {@link OnInvokeActivity(TurnContext(InvokeActivity))} method + * receives an Invoke with a {@link InvokeActivity.name} of + * `adaptiveCard/action`, it calls this method. + */ + protected CompletableFuture onAdaptiveCardInvoke( + TurnContext turnContext, AdaptiveCardInvokeValue invokeValue) { + return Async.completeExceptionally(new InvokeResponseException(HttpURLConnection.HTTP_NOT_IMPLEMENTED)); + } + + /** + * Override this in a derived class to provide logic specific to + * ActivityTypes.END_OF_CONVERSATION activities. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onEndOfConversationActivity(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic specific to + * ActivityTypes.Typing activities. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTypingActivity(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when an activity other than a message, conversation update, or event + * is received when the base behavior of {@link #onTurn(TurnContext)} is used. + * + *

+ * If overridden, this could potentially respond to any of the other activity + * types like + * {@link com.microsoft.bot.schema.ActivityTypes#CONTACT_RELATION_UPDATE} or + * {@link com.microsoft.bot.schema.ActivityTypes#END_OF_CONVERSATION}. + *

+ * + *

+ * By default, this method does nothing. + *

+ * + *

+ * When the {@link #onTurn(TurnContext)} method receives an activity that is not + * a message, conversation update, message reaction, or event activity, it calls + * this method. + *

+ * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onUnrecognizedActivityType(TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + private AdaptiveCardInvokeValue getAdaptiveCardInvokeValue(Activity activity) throws InvokeResponseException { + if (activity.getValue() == null) { + AdaptiveCardInvokeResponse response = createAdaptiveCardInvokeErrorResponse( + HttpURLConnection.HTTP_BAD_REQUEST, "BadRequest", "Missing value property"); + throw new InvokeResponseException(HttpURLConnection.HTTP_BAD_REQUEST, response); + } + + AdaptiveCardInvokeValue invokeValue = Serialization.getAs(activity.getValue(), AdaptiveCardInvokeValue.class); + if (invokeValue == null) { + AdaptiveCardInvokeResponse response = createAdaptiveCardInvokeErrorResponse( + HttpURLConnection.HTTP_BAD_REQUEST, "BadRequest", "Value property instanceof not properly formed"); + throw new InvokeResponseException(HttpURLConnection.HTTP_BAD_REQUEST, response); + } + + if (invokeValue.getAction() == null) { + AdaptiveCardInvokeResponse response = createAdaptiveCardInvokeErrorResponse( + HttpURLConnection.HTTP_BAD_REQUEST, "BadRequest", "Missing action property"); + throw new InvokeResponseException(HttpURLConnection.HTTP_BAD_REQUEST, response); + } + + if (!invokeValue.getAction().getType().equals("Action.Execute")) { + AdaptiveCardInvokeResponse response = createAdaptiveCardInvokeErrorResponse( + HttpURLConnection.HTTP_BAD_REQUEST, "NotSupported", + String.format("The action '%s'is not supported.", invokeValue.getAction().getType())); + throw new InvokeResponseException(HttpURLConnection.HTTP_BAD_REQUEST, response); + } + + return invokeValue; + } + + private AdaptiveCardInvokeResponse createAdaptiveCardInvokeErrorResponse( + Integer statusCode, + String code, + String message + ) { + AdaptiveCardInvokeResponse adaptiveCardInvokeResponse = new AdaptiveCardInvokeResponse(); + adaptiveCardInvokeResponse.setStatusCode(statusCode); + adaptiveCardInvokeResponse.setType("application/vnd.getmicrosoft().error"); + com.microsoft.bot.schema.Error error = new com.microsoft.bot.schema.Error(); + error.setCode(code); + error.setMessage(message); + adaptiveCardInvokeResponse.setValue(error); + return adaptiveCardInvokeResponse; + } + + + /** + * InvokeResponse Exception. + */ + protected class InvokeResponseException extends Exception { + + private int statusCode; + private Object body; + + /** + * Initializes new instance with HTTP status code value. + * + * @param withStatusCode The HTTP status code. + */ + public InvokeResponseException(int withStatusCode) { + this(withStatusCode, null); + } + + /** + * Initializes new instance with HTTP status code value. + * + * @param withStatusCode The HTTP status code. + * @param withBody The body. Can be null. + */ + public InvokeResponseException(int withStatusCode, Object withBody) { + statusCode = withStatusCode; + body = withBody; + } + + /** + * Returns an InvokeResponse based on this exception. + * + * @return The InvokeResponse value. + */ + public InvokeResponse createInvokeResponse() { + return new InvokeResponse(statusCode, body); + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/AutoSaveStateMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/AutoSaveStateMiddleware.java new file mode 100644 index 000000000..93be0cce7 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/AutoSaveStateMiddleware.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +package com.microsoft.bot.builder; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +/** + * Middleware to automatically call .SaveChanges() at the end of the turn for + * all BotState class it is managing. + */ +public class AutoSaveStateMiddleware implements Middleware { + /** + * The list of state management objects managed by this object. + */ + private BotStateSet botStateSet; + + /** + * Initializes a new instance of the AutoSaveStateMiddleware class. + * + * @param botStates Initial list of {@link BotState} objects to manage. + */ + public AutoSaveStateMiddleware(BotState... botStates) { + botStateSet = new BotStateSet(Arrays.asList(botStates)); + } + + /** + * Initializes a new instance of the AutoSaveStateMiddleware class. + * + * @param withBotStateSet Initial {@link BotStateSet} object to manage. + */ + public AutoSaveStateMiddleware(BotStateSet withBotStateSet) { + botStateSet = withBotStateSet; + } + + /** + * Gets the list of state management objects managed by this object. + * + * @return The state management objects managed by this object. + */ + public BotStateSet getBotStateSet() { + return botStateSet; + } + + /** + * Gets the list of state management objects managed by this object. + * + * @param withBotStateSet The state management objects managed by this object. + */ + public void setBotStateSet(BotStateSet withBotStateSet) { + botStateSet = withBotStateSet; + } + + /** + * Add a BotState to the list of sources to load. + * + * @param botState botState to manage. + * @return botstateset for chaining more .use(). + */ + public AutoSaveStateMiddleware add(BotState botState) { + if (botState == null) { + throw new IllegalArgumentException("botState cannot be null"); + } + + botStateSet.add(botState); + return this; + } + + /** + * Middleware implementation which calls savesChanges automatically at the end + * of the turn. + * + * @param turnContext The context object for this turn. + * @param next The delegate to call to continue the bot middleware + * pipeline. + * @return A task representing the asynchronous operation. + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + return next.next().thenCompose(result -> botStateSet.saveAllChanges(turnContext)); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Bot.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Bot.java new file mode 100644 index 000000000..b73cfa776 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Bot.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * Represents a bot that can operate on incoming activities. + */ +public interface Bot { + /** + * When implemented in a bot, handles an incoming activity. + * + * @param turnContext The context object for this turn. Provides information + * about the incoming activity, and other data needed to + * process the activity. + * @return A task that represents the work queued to execute. + */ + CompletableFuture onTurn(TurnContext turnContext); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java new file mode 100644 index 000000000..94cdf5662 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; + +/** + * Represents a bot adapter that can connect a bot to a service endpoint. This + * class is abstract. + *

+ * The bot adapter encapsulates authentication processes and sends activities to + * and receives activities from the Bot Connector Service. When your bot + * receives an activity, the adapter creates a context object, passes it to your + * bot's application logic, and sends responses back to the user's channel. + *

+ *

+ * Use {@link #use(Middleware)} to add {@link Middleware} objects to your + * adapter’s middleware collection. The adapter processes and directs incoming + * activities in through the bot middleware pipeline to your bot’s logic and + * then back out again. As each activity flows in and out of the bot, each piece + * of middleware can inspect or act upon the activity, both before and after the + * bot logic runs. + *

+ * + * {@link TurnContext} {@link Activity} {@link Bot} {@link Middleware} + */ +public abstract class BotAdapter { + /** + * Key to store bot claims identity. + */ + public static final String BOT_IDENTITY_KEY = "BotIdentity"; + + /** + * Key to store bot oauth scope. + */ + public static final String OAUTH_SCOPE_KEY = "Microsoft.Bot.Builder.BotAdapter.OAuthScope"; + + /** + * Key to store bot oauth client. + */ + public static final String OAUTH_CLIENT_KEY = "OAuthClient"; + + /** + * The collection of middleware in the adapter's pipeline. + */ + private final MiddlewareSet middlewareSet = new MiddlewareSet(); + + /** + * Error handler that can catch exceptions in the middleware or application. + */ + private OnTurnErrorHandler onTurnError; + + /** + * Gets the error handler that can catch exceptions in the middleware or + * application. + * + * @return An error handler that can catch exceptions in the middleware or + * application. + */ + public OnTurnErrorHandler getOnTurnError() { + return onTurnError; + } + + /** + * Sets the error handler that can catch exceptions in the middleware or + * application. + * + * @param withTurnError An error handler that can catch exceptions in the + * middleware or application. + */ + public void setOnTurnError(OnTurnErrorHandler withTurnError) { + onTurnError = withTurnError; + } + + /** + * Gets the collection of middleware in the adapter's pipeline. + * + * @return The middleware collection for the pipeline. + */ + protected MiddlewareSet getMiddlewareSet() { + return middlewareSet; + } + + /** + * Adds middleware to the adapter's pipeline. + * + * @param middleware The middleware to add. + * @return The updated adapter object. Middleware is added to the adapter at + * initialization time. For each turn, the adapter calls middleware in + * the order in which you added it. + */ + public BotAdapter use(Middleware middleware) { + middlewareSet.use(middleware); + return this; + } + + /** + * When overridden in a derived class, sends activities to the conversation. + * + * @param context The context object for the turn. + * @param activities The activities to send. + * @return A task that represents the work queued to execute. If the activities + * are successfully sent, the task result contains an array of + * {@link ResourceResponse} objects containing the IDs that the + * receiving channel assigned to the activities. + * {@link TurnContext#onSendActivities(SendActivitiesHandler)} + */ + public abstract CompletableFuture sendActivities( + TurnContext context, + List activities + ); + + /** + * When overridden in a derived class, replaces an existing activity in the + * conversation. + * + * @param context The context object for the turn. + * @param activity New replacement activity. + * @return A task that represents the work queued to execute. If the activity is + * successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving + * channel assigned to the activity. + *

+ * Before calling this, set the ID of the replacement activity to the ID + * of the activity to replace. + *

+ * {@link TurnContext#onUpdateActivity(UpdateActivityHandler)} + */ + public abstract CompletableFuture updateActivity( + TurnContext context, + Activity activity + ); + + /** + * When overridden in a derived class, deletes an existing activity in the + * conversation. + * + * @param context The context object for the turn. + * @param reference Conversation reference for the activity to delete. + * @return A task that represents the work queued to execute. The + * {@link ConversationReference#getActivityId} of the conversation + * reference identifies the activity to delete. + * {@link TurnContext#onDeleteActivity(DeleteActivityHandler)} + */ + public abstract CompletableFuture deleteActivity( + TurnContext context, + ConversationReference reference + ); + + /** + * Starts activity processing for the current bot turn. + * + * The adapter calls middleware in the order in which you added it. The adapter + * passes in the context object for the turn and a next delegate, and the + * middleware calls the delegate to pass control to the next middleware in the + * pipeline. Once control reaches the end of the pipeline, the adapter calls the + * {@code callback} method. If a middleware component does not call the next + * delegate, the adapter does not call any of the subsequent middleware’s + * {@link Middleware#onTurn(TurnContext, NextDelegate)} methods or the callback + * method, and the pipeline short circuits. + * + *

+ * When the turn is initiated by a user activity (reactive messaging), the + * callback method will be a reference to the bot's + * {@link Bot#onTurn(TurnContext)} method. When the turn is initiated by a call + * to + * {@link #continueConversation(String, ConversationReference, BotCallbackHandler)} + * (proactive messaging), the callback method is the callback method that was + * provided in the call. + *

+ * + * @param context The turn's context object. + * @param callback A callback method to run at the end of the pipeline. + * @return A task that represents the work queued to execute. + * @throws NullPointerException {@code context} is null. + */ + protected CompletableFuture runPipeline( + TurnContext context, + BotCallbackHandler callback + ) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + + // Call any registered Middleware Components looking for ReceiveActivity() + if (context.getActivity() != null) { + if (!StringUtils.isEmpty(context.getActivity().getLocale())) { + + Locale.setDefault(Locale.forLanguageTag(context.getActivity().getLocale())); + + context.setLocale(context.getActivity().getLocale()); + } + + return middlewareSet.receiveActivityWithStatus(context, callback) + .exceptionally(exception -> { + if (onTurnError != null) { + return onTurnError.invoke(context, exception).join(); + } + + throw new CompletionException(exception); + }); + } else { + // call back to caller on proactive case + if (callback != null) { + return callback.invoke(context); + } + + return CompletableFuture.completedFuture(null); + } + } + + /** + * Sends a proactive message to a conversation. + * + * @param botAppId The application ID of the bot. This parameter is ignored in + * single tenant the Adapters (Console, Test, etc) but is + * critical to the BotFrameworkAdapter which is multi-tenant + * aware. + * @param reference A reference to the conversation to continue. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. Call this method + * to proactively send a message to a conversation. Most channels + * require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + * + * {@link #runPipeline(TurnContext, BotCallbackHandler)} + */ + public CompletableFuture continueConversation( + String botAppId, + ConversationReference reference, + BotCallbackHandler callback + ) { + CompletableFuture pipelineResult = new CompletableFuture<>(); + + try (TurnContextImpl context = + new TurnContextImpl(this, reference.getContinuationActivity())) { + pipelineResult = runPipeline(context, callback); + } catch (Exception e) { + pipelineResult.completeExceptionally(e); + } + + return pipelineResult; + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param claimsIdentity A ClaimsIdentity reference for the conversation. + * @param reference A reference to the conversation to continue. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + BotCallbackHandler callback + ) { + return Async.completeExceptionally(new NotImplementedException("continueConversation")); + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param claimsIdentity A ClaimsIdentity reference for the conversation. + * @param reference A reference to the conversation to continue. + * @param audience A value signifying the recipient of the proactive + * message. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + String audience, + BotCallbackHandler callback + ) { + return Async.completeExceptionally(new NotImplementedException("continueConversation")); + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param botId The application ID of the bot. This parameter is ignored in single tenant + * the Adapters (Console, Test, etc) but is critical to the BotFrameworkAdapter + * which is multi-tenant aware. + * @param continuationActivity An Activity with the appropriate ConversationReference with + * which to continue the conversation. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + String botId, + Activity continuationActivity, + BotCallbackHandler callback + ) { + return Async.completeExceptionally(new NotImplementedException("continueConversation")); + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param claimsIdentity A ClaimsIdentity for the conversation. + * @param continuationActivity An Activity with the appropriate ConversationReference with + * which to continue the conversation. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + Activity continuationActivity, + BotCallbackHandler callback + ) { + return Async.completeExceptionally(new NotImplementedException("continueConversation")); + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param claimsIdentity A ClaimsIdentity for the conversation. + * @param continuationActivity An Activity with the appropriate ConversationReference with + * which to continue the conversation. + * @param audience A value signifying the recipient of the proactive + * message. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + Activity continuationActivity, + String audience, + BotCallbackHandler callback + ) { + return Async.completeExceptionally(new NotImplementedException("continueConversation")); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAssert.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAssert.java new file mode 100644 index 000000000..63031a3ae --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAssert.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides methods for debugging Bot Builder code. + */ +public final class BotAssert { + /** + * This class can't be created. + */ + private BotAssert() { + + } + + /** + * Checks that an activity object is not {@code null}. + * + * @param activity The activity object. + * @throws NullPointerException {@code activity} is {@code null}. + */ + public static void activityNotNull(Activity activity) { + if (activity == null) { + throw new IllegalArgumentException("Activity"); + } + } + + /** + * Checks that a context object is not {@code null}. + * + * @param context The context object. + * @throws NullPointerException {@code context} is {@code null}. + */ + public static void contextNotNull(TurnContext context) { + if (context == null) { + throw new IllegalArgumentException("TurnContext"); + } + } + + /** + * Checks that a conversation reference object is not {@code null}. + * + * @param reference The conversation reference object. + * @throws NullPointerException {@code reference} is {@code null}. + */ + public static void conversationReferenceNotNull(ConversationReference reference) { + if (reference == null) { + throw new IllegalArgumentException("ConversationReference"); + } + } + + /** + * Checks that an activity collection is not {@code null}. + * + * @param activities The activities. + * @throws NullPointerException {@code activities} is {@code null}. + */ + public static void activityListNotNull(List activities) { + if (activities == null) { + throw new NullPointerException("List"); + } + } + + /** + * Checks that a middleware object is not {@code null}. + * + * @param middleware The middleware object. + * @throws NullPointerException {@code middleware} is {@code null}. + */ + public static void middlewareNotNull(Middleware middleware) { + if (middleware == null) { + throw new NullPointerException("Middleware"); + } + } + + /** + * Checks that a middleware collection is not {@code null}. + * + * @param middleware The middleware. + * @throws NullPointerException {@code middleware} is {@code null}. + */ + public static void middlewareNotNull(ArrayList middleware) { + if (middleware == null) { + throw new NullPointerException("List"); + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotCallbackHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotCallbackHandler.java new file mode 100644 index 000000000..21d8fcafc --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotCallbackHandler.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * The callback delegate for application code. + */ +@FunctionalInterface +public interface BotCallbackHandler { + /** + * The callback delegate for application code. + * + * @param turnContext The turn context. + * @return A Task representing the asynchronous operation. + */ + CompletableFuture invoke(TurnContext turnContext); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java new file mode 100644 index 000000000..8bc110c2a --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java @@ -0,0 +1,1922 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.integration.AdapterIntegration; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.connector.OAuthClient; +import com.microsoft.bot.connector.OAuthClientConfig; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ChannelProvider; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.CredentialProvider; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.MicrosoftGovernmentAppCredentials; +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; +import com.microsoft.bot.connector.authentication.SkillValidation; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.connector.rest.RestOAuthClient; +import com.microsoft.bot.schema.AadResourceUrls; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityEventNames; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.CallerIdConstants; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.ExpectedReplies; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenExchangeState; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; +import com.microsoft.bot.restclient.retry.RetryStrategy; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.lang3.StringUtils; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A bot adapter that can connect a bot to a service endpoint. + * + * The bot adapter encapsulates authentication processes and sends activities to + * and receives activities from the Bot Connector Service. When your bot + * receives an activity, the adapter creates a context object, passes it to your + * bot's application logic, and sends responses back to the user's channel. + *

+ * Use {@link #use(Middleware)} to add {@link Middleware} objects to your + * adapter’s middleware collection. The adapter processes and directs incoming + * activities in through the bot middleware pipeline to your bot’s logic and + * then back out again. As each activity flows in and out of the bot, each piece + * of middleware can inspect or act upon the activity, both before and after the + * bot logic runs. + *

+ *

+ * {@link TurnContext} {@link Activity} {@link Bot} {@link Middleware} + */ +public class BotFrameworkAdapter extends BotAdapter + implements AdapterIntegration, UserTokenProvider, ConnectorClientBuilder { + /** + * Key to store InvokeResponse. + */ + public static final String INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse"; + + /** + * Key to store ConnectorClient. + */ + public static final String CONNECTOR_CLIENT_KEY = "ConnectorClient"; + + /** + * Key to store TeamsConnectorClient. For testing only. + */ + public static final String TEAMSCONNECTOR_CLIENT_KEY = "TeamsConnectorClient"; + + private AppCredentials appCredentials; + + /** + * The credential provider. + */ + private final CredentialProvider credentialProvider; + + /** + * The channel provider. + */ + private ChannelProvider channelProvider; + + /** + * The authentication configuration. + */ + private AuthenticationConfiguration authConfiguration; + + /** + * Rest RetryStrategy. + */ + private final RetryStrategy connectorClientRetryStrategy; + + /** + * AppCredentials dictionary. + */ + private Map appCredentialMap = new ConcurrentHashMap<>(); + + /** + * ConnectorClient cache. + */ + private Map connectorClients = new ConcurrentHashMap<>(); + + /** + * OAuthClient cache. + */ + private Map oAuthClients = new ConcurrentHashMap<>(); + + /** + * Initializes a new instance of the {@link BotFrameworkAdapter} class, using a + * credential provider. + * + * @param withCredentialProvider The credential provider. + */ + public BotFrameworkAdapter(CredentialProvider withCredentialProvider) { + this(withCredentialProvider, null, null, null); + } + + /** + * Initializes a new instance of the {@link BotFrameworkAdapter} class, using a + * credential provider. + * + * @param withCredentialProvider The credential provider. + * @param withChannelProvider The channel provider. + * @param withRetryStrategy Retry policy for retrying HTTP operations. + * @param withMiddleware The middleware to initially add to the adapter. + */ + public BotFrameworkAdapter( + CredentialProvider withCredentialProvider, + ChannelProvider withChannelProvider, + RetryStrategy withRetryStrategy, + Middleware withMiddleware + ) { + this( + withCredentialProvider, + new AuthenticationConfiguration(), + withChannelProvider, + withRetryStrategy, + withMiddleware + ); + } + + /** + * Initializes a new instance of the {@link BotFrameworkAdapter} class, using a + * credential provider. + * + * @param withCredentialProvider The credential provider. + * @param withAuthConfig The authentication configuration. + * @param withChannelProvider The channel provider. + * @param withRetryStrategy Retry policy for retrying HTTP operations. + * @param withMiddleware The middleware to initially add to the adapter. + */ + public BotFrameworkAdapter( + CredentialProvider withCredentialProvider, + AuthenticationConfiguration withAuthConfig, + ChannelProvider withChannelProvider, + RetryStrategy withRetryStrategy, + Middleware withMiddleware + ) { + if (withCredentialProvider == null) { + throw new IllegalArgumentException("CredentialProvider cannot be null"); + } + + if (withAuthConfig == null) { + throw new IllegalArgumentException("AuthenticationConfiguration cannot be null"); + } + + credentialProvider = withCredentialProvider; + channelProvider = withChannelProvider; + connectorClientRetryStrategy = withRetryStrategy; + authConfiguration = withAuthConfig; + + // Relocate the tenantId field used by MS Teams to a new location (from + // channelData to conversation) + // This will only occur on activities from teams that include tenant info in + // channelData but NOT in + // conversation, thus should be future friendly. However, once the transition is + // complete. we can + // remove this. + use(new TenantIdWorkaroundForTeamsMiddleware()); + + if (withMiddleware != null) { + use(withMiddleware); + } + } + + /** + * Initializes a new instance of the {@link BotFrameworkAdapter} class, using a + * credential provider. + * + * @param withCredentials The credentials to use. + * @param withAuthConfig The authentication configuration. + * @param withChannelProvider The channel provider. + * @param withRetryStrategy Retry policy for retrying HTTP operations. + * @param withMiddleware The middleware to initially add to the adapter. + */ + public BotFrameworkAdapter( + AppCredentials withCredentials, + AuthenticationConfiguration withAuthConfig, + ChannelProvider withChannelProvider, + RetryStrategy withRetryStrategy, + Middleware withMiddleware + ) { + if (withCredentials == null) { + throw new IllegalArgumentException("credentials"); + } + appCredentials = withCredentials; + + credentialProvider = new SimpleCredentialProvider(withCredentials.getAppId(), null); + channelProvider = withChannelProvider; + connectorClientRetryStrategy = withRetryStrategy; + + if (withAuthConfig == null) { + throw new IllegalArgumentException("authConfig"); + } + authConfiguration = withAuthConfig; + + // Relocate the tenantId field used by MS Teams to a new location (from + // channelData to conversation) + // This will only occur on activities from teams that include tenant info in + // channelData but NOT in + // conversation, thus should be future friendly. However, once the transition is + // complete. we can + // remove this. + use(new TenantIdWorkaroundForTeamsMiddleware()); + + if (withMiddleware != null) { + use(withMiddleware); + } + } + + /** + * Sends a proactive message from the bot to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ *

+ * This overload differers from the Node implementation by requiring the BotId + * to be passed in. The .Net code allows multiple bots to be hosted in a single + * adapter which isn't something supported by Node. + *

+ * + * {@link #processActivity(String, Activity, BotCallbackHandler)} + * {@link BotAdapter#runPipeline(TurnContext, BotCallbackHandler)} + * + * @param botAppId The application ID of the bot. This is the appId returned by + * Portal registration, and is generally found in the + * "MicrosoftAppId" parameter in appSettings.json. + * @param reference A reference to the conversation to continue. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException botAppId, reference, or callback is null. + */ + @Override + public CompletableFuture continueConversation( + String botAppId, + ConversationReference reference, + BotCallbackHandler callback + ) { + if (reference == null) { + return Async.completeExceptionally(new IllegalArgumentException("reference")); + } + + if (callback == null) { + return Async.completeExceptionally(new IllegalArgumentException("callback")); + } + + botAppId = botAppId == null ? "" : botAppId; + + // Hand craft Claims Identity. + // Adding claims for both Emulator and Channel. + HashMap claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, botAppId); + claims.put(AuthenticationConstants.APPID_CLAIM, botAppId); + + ClaimsIdentity claimsIdentity = new ClaimsIdentity("ExternalBearer", claims); + String audience = getBotFrameworkOAuthScope(); + + return continueConversation(claimsIdentity, reference, audience, callback); + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param claimsIdentity A ClaimsIdentity reference for the conversation. + * @param reference A reference to the conversation to continue. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + BotCallbackHandler callback + ) { + return continueConversation(claimsIdentity, reference, getBotFrameworkOAuthScope(), callback); + } + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param claimsIdentity A ClaimsIdentity reference for the conversation. + * @param reference A reference to the conversation to continue. + * @param audience A value signifying the recipient of the proactive + * message. + * @param callback The method to call for the result bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + String audience, + BotCallbackHandler callback + ) { + if (claimsIdentity == null) { + return Async.completeExceptionally(new IllegalArgumentException("claimsIdentity")); + } + + if (reference == null) { + return Async.completeExceptionally(new IllegalArgumentException("reference")); + } + + if (callback == null) { + return Async.completeExceptionally(new IllegalArgumentException("callback")); + } + + if (StringUtils.isEmpty(audience)) { + return Async.completeExceptionally(new IllegalArgumentException("audience cannot be null or empty")); + } + + CompletableFuture pipelineResult = new CompletableFuture<>(); + + try (TurnContextImpl context = new TurnContextImpl(this, reference.getContinuationActivity())) { + context.getTurnState().add(BOT_IDENTITY_KEY, claimsIdentity); + context.getTurnState().add(OAUTH_SCOPE_KEY, audience); + + return createConnectorClient(reference.getServiceUrl(), claimsIdentity, audience) + .thenCompose(connectorClient -> { + context.getTurnState().add(CONNECTOR_CLIENT_KEY, connectorClient); + return runPipeline(context, callback); + }); + } catch (Exception e) { + pipelineResult.completeExceptionally(e); + } + + return pipelineResult; + } + + /** + * Adds middleware to the adapter's pipeline. + * + * Middleware is added to the adapter at initialization time. For each turn, the + * adapter calls middleware in the order in which you added it. + * + * @param middleware The middleware to add. + * @return The updated adapter object. + */ + public BotFrameworkAdapter use(Middleware middleware) { + getMiddlewareSet().use(middleware); + return this; + } + + /** + * Creates a turn context and runs the middleware pipeline for an incoming + * activity. + * + * @param authHeader The HTTP authentication header of the request. + * @param activity The incoming activity. + * @param callback The code to run at the end of the adapter's middleware + * pipeline. + * @return A task that represents the work queued to execute. If the activity + * type was 'Invoke' and the corresponding key (channelId + activityId) + * was found then an InvokeResponse is returned, otherwise null is + * returned. + * @throws IllegalArgumentException Activity is null. + */ + public CompletableFuture processActivity( + String authHeader, + Activity activity, + BotCallbackHandler callback + ) { + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity")); + } + + return JwtTokenValidation + .authenticateRequest(activity, authHeader, credentialProvider, channelProvider, authConfiguration) + .thenCompose(claimsIdentity -> processActivity(claimsIdentity, activity, callback)); + } + + /** + * Creates a turn context and runs the middleware pipeline for an incoming + * activity. + * + * @param identity A {@link ClaimsIdentity} for the request. + * @param activity The incoming activity. + * @param callback The code to run at the end of the adapter's middleware + * pipeline. + * @return A task that represents the work queued to execute. If the activity + * type was 'Invoke' and the corresponding key (channelId + activityId) + * was found then an InvokeResponse is returned, otherwise null is + * returned. + * @throws IllegalArgumentException Activity is null. + */ + public CompletableFuture processActivity( + ClaimsIdentity identity, + Activity activity, + BotCallbackHandler callback + ) { + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity")); + } + + CompletableFuture pipelineResult = new CompletableFuture<>(); + + try (TurnContextImpl context = new TurnContextImpl(this, activity)) { + activity.setCallerId(generateCallerId(identity).join()); + context.getTurnState().add(BOT_IDENTITY_KEY, identity); + + // The OAuthScope is also stored on the TurnState to get the correct + // AppCredentials if fetching a token is required. + String scope = SkillValidation.isSkillClaim(identity.claims()) + ? String.format("%s/.default", JwtTokenValidation.getAppIdFromClaims(identity.claims())) + : getBotFrameworkOAuthScope(); + + context.getTurnState().add(OAUTH_SCOPE_KEY, scope); + + pipelineResult = createConnectorClient(activity.getServiceUrl(), identity, scope) + + // run pipeline + .thenCompose(connectorClient -> { + context.getTurnState().add(CONNECTOR_CLIENT_KEY, connectorClient); + return runPipeline(context, callback); + }) + .thenCompose(result -> { + // Handle ExpectedReplies scenarios where the all the activities have been + // buffered and sent back at once in an invoke response. + if ( + DeliveryModes + .fromString(context.getActivity().getDeliveryMode()) == DeliveryModes.EXPECT_REPLIES + ) { + return CompletableFuture.completedFuture( + new InvokeResponse( + HttpURLConnection.HTTP_OK, + new ExpectedReplies(context.getBufferedReplyActivities()) + ) + ); + } + + // Handle Invoke scenarios, which deviate from the request/response model in + // that the Bot will return a specific body and return code. + if (activity.isType(ActivityTypes.INVOKE)) { + Activity invokeResponse = context.getTurnState().get(INVOKE_RESPONSE_KEY); + if (invokeResponse == null) { + return CompletableFuture + .completedFuture(new InvokeResponse(HttpURLConnection.HTTP_NOT_IMPLEMENTED, null)); + } else { + return CompletableFuture.completedFuture((InvokeResponse) invokeResponse.getValue()); + } + } + + // For all non-invoke scenarios, the HTTP layers above don't have to mess + // with the Body and return codes. + return CompletableFuture.completedFuture(null); + }); + } catch (Exception e) { + pipelineResult.completeExceptionally(e); + } + + return pipelineResult; + } + + @SuppressWarnings("PMD") + private CompletableFuture generateCallerId(ClaimsIdentity claimsIdentity) { + return credentialProvider.isAuthenticationDisabled().thenApply(is_auth_disabled -> { + // Is the bot accepting all incoming messages? + if (is_auth_disabled) { + return null; + } + + // Is the activity from another bot? + if (SkillValidation.isSkillClaim(claimsIdentity.claims())) { + return String.format( + "%s%s", + CallerIdConstants.BOT_TO_BOT_PREFIX, + JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()) + ); + } + + // Is the activity from Public Azure? + if (channelProvider == null || channelProvider.isPublicAzure()) { + return CallerIdConstants.PUBLIC_AZURE_CHANNEL; + } + + // Is the activity from Azure Gov? + if (channelProvider != null && channelProvider.isGovernment()) { + return CallerIdConstants.US_GOV_CHANNEL; + } + + // Return null so that the callerId is cleared. + return null; + }); + } + + /** + * Sends activities to the conversation. + * + * @param context The context object for the turn. + * @param activities The activities to send. + * @return A task that represents the work queued to execute. If the activities + * are successfully sent, the task result contains an array of + * {@link ResourceResponse} objects containing the IDs that the + * receiving channel assigned to the activities. + * + * {@link TurnContext#onSendActivities(SendActivitiesHandler)} + */ + @SuppressWarnings("checkstyle:EmptyBlock, checkstyle:linelength") + @Override + public CompletableFuture sendActivities(TurnContext context, List activities) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("context")); + } + + if (activities == null) { + return Async.completeExceptionally(new IllegalArgumentException("activities")); + } + + if (activities.size() == 0) { + return Async.completeExceptionally( + new IllegalArgumentException("Expecting one or more activities, but the array was empty.") + ); + } + + return CompletableFuture.supplyAsync(() -> { + ResourceResponse[] responses = new ResourceResponse[activities.size()]; + + /* + * NOTE: we're using for here (vs. foreach) because we want to simultaneously + * index into the activities array to get the activity to process as well as use + * that index to assign the response to the responses array and this is the most + * cost effective way to do that. + */ + for (int index = 0; index < activities.size(); index++) { + Activity activity = activities.get(index); + + // Clients and bots SHOULD NOT include an id field in activities they generate. + activity.setId(null); + + ResourceResponse response; + if (activity.isType(ActivityTypes.DELAY)) { + // The Activity Schema doesn't have a delay type build in, so it's simulated + // here in the Bot. This matches the behavior in the Node connector. + int delayMs = (int) activity.getValue(); + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + // No need to create a response. One will be created below. + response = null; + } else if (activity.isType(ActivityTypes.INVOKE_RESPONSE)) { + context.getTurnState().add(INVOKE_RESPONSE_KEY, activity); + // No need to create a response. One will be created below. + response = null; + } else if ( + activity.isType(ActivityTypes.TRACE) + && !StringUtils.equals(activity.getChannelId(), Channels.EMULATOR) + ) { + // if it is a Trace activity we only send to the channel if it's the emulator. + response = null; + } else if (!StringUtils.isEmpty(activity.getReplyToId())) { + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + response = connectorClient.getConversations().replyToActivity(activity).join(); + } else { + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + response = connectorClient.getConversations().sendToConversation(activity).join(); + } + + // If No response is set, then default to a "simple" response. This can't really + // be done above, as there are cases where the ReplyTo/SendTo methods will also + // return null (See below) so the check has to happen here. + // + // Note: In addition to the Invoke / Delay / Activity cases, this code also + // applies with Skype and Teams with regards to typing events. When sending a + // typing event in these channels they do not return a RequestResponse which + // causes the bot to blow up. + // + // https://github.com/Microsoft/botbuilder-dotnet/issues/460 + // bug report : https://github.com/Microsoft/botbuilder-dotnet/issues/465 + if (response == null) { + response = new ResourceResponse((activity.getId() == null) ? "" : activity.getId()); + } + + responses[index] = response; + } + + return responses; + }, ExecutorFactory.getExecutor()); + } + + /** + * Replaces an existing activity in the conversation. + * + * @param context The context object for the turn. + * @param activity New replacement activity. + * @return A task that represents the work queued to execute. If the activity is + * successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving + * channel assigned to the activity. + *

+ * Before calling this, set the ID of the replacement activity to the ID + * of the activity to replace. + *

+ * {@link TurnContext#onUpdateActivity(UpdateActivityHandler)} + */ + @Override + public CompletableFuture updateActivity(TurnContext context, Activity activity) { + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + return connectorClient.getConversations().updateActivity(activity); + } + + /** + * Deletes an existing activity in the conversation. + * + * @param context The context object for the turn. + * @param reference Conversation reference for the activity to delete. + * @return A task that represents the work queued to execute. + * {@link TurnContext#onDeleteActivity(DeleteActivityHandler)} + */ + @Override + public CompletableFuture deleteActivity(TurnContext context, ConversationReference reference) { + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + return connectorClient.getConversations() + .deleteActivity(reference.getConversation().getId(), reference.getActivityId()); + } + + /** + * Deletes a member from the current conversation. + * + * @param context The context object for the turn. + * @param memberId ID of the member to delete from the conversation + * @return A task that represents the work queued to execute. + */ + public CompletableFuture deleteConversationMember(TurnContextImpl context, String memberId) { + if (context.getActivity().getConversation() == null) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.deleteConversationMember(): missing conversation") + ); + } + + if (StringUtils.isEmpty(context.getActivity().getConversation().getId())) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.deleteConversationMember(): missing conversation.id") + ); + } + + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + String conversationId = context.getActivity().getConversation().getId(); + return connectorClient.getConversations().deleteConversationMember(conversationId, memberId); + } + + /** + * Lists the members of a given activity. + * + * @param context The context object for the turn. + * @return List of Members of the activity + */ + public CompletableFuture> getActivityMembers(TurnContextImpl context) { + return getActivityMembers(context, null); + } + + /** + * Lists the members of a given activity. + * + * @param context The context object for the turn. + * @param activityId (Optional) Activity ID to enumerate. If not specified the + * current activities ID will be used. + * @return List of Members of the activity + */ + public CompletableFuture> getActivityMembers(TurnContextImpl context, String activityId) { + // If no activity was passed in, use the current activity. + if (activityId == null) { + activityId = context.getActivity().getId(); + } + + if (context.getActivity().getConversation() == null) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation") + ); + } + + if (StringUtils.isEmpty(context.getActivity().getConversation().getId())) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id") + ); + } + + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + String conversationId = context.getActivity().getConversation().getId(); + + return connectorClient.getConversations().getActivityMembers(conversationId, activityId); + } + + /** + * Lists the members of the current conversation. + * + * @param context The context object for the turn. + * @return List of Members of the current conversation + */ + public CompletableFuture> getConversationMembers(TurnContextImpl context) { + if (context.getActivity().getConversation() == null) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation") + ); + } + + if (StringUtils.isEmpty(context.getActivity().getConversation().getId())) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.GetActivityMembers(): missing conversation.id") + ); + } + + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + String conversationId = context.getActivity().getConversation().getId(); + + return connectorClient.getConversations().getConversationMembers(conversationId); + } + + /** + * Lists the Conversations in which this bot has participated for a given + * channel server. The channel server returns results in pages and each page + * will include a `continuationToken` that can be used to fetch the next page of + * results from the server. + * + * @param serviceUrl The URL of the channel server to query. This can be + * retrieved from `context.activity.serviceUrl`. + * @param credentials The credentials needed for the Bot to connect to + * the.services(). + * @return List of Members of the current conversation + *

+ * This overload may be called from outside the context of a + * conversation, as only the Bot's ServiceUrl and credentials are + * required. + */ + public CompletableFuture getConversations( + String serviceUrl, + MicrosoftAppCredentials credentials + ) { + return getConversations(serviceUrl, credentials, null); + } + + /** + * Lists the Conversations in which this bot has participated for a given + * channel server. The channel server returns results in pages and each page + * will include a `continuationToken` that can be used to fetch the next page of + * results from the server. + * + * This overload may be called from outside the context of a conversation, as + * only the Bot's ServiceUrl and credentials are required. + * + * @param serviceUrl The URL of the channel server to query. This can be + * retrieved from `context.activity.serviceUrl`. + * @param credentials The credentials needed for the Bot to connect to + * the.services(). + * @param continuationToken The continuation token from the previous page of + * results. + * @return List of Members of the current conversation + */ + public CompletableFuture getConversations( + String serviceUrl, + MicrosoftAppCredentials credentials, + String continuationToken + ) { + if (StringUtils.isEmpty(serviceUrl)) { + return Async.completeExceptionally(new IllegalArgumentException("serviceUrl")); + } + + if (credentials == null) { + return Async.completeExceptionally(new IllegalArgumentException("credentials")); + } + + return getOrCreateConnectorClient(serviceUrl, credentials) + .thenCompose(connectorClient -> connectorClient.getConversations().getConversations(continuationToken)); + } + + /** + * Lists the Conversations in which this bot has participated for a given + * channel server. The channel server returns results in pages and each page + * will include a `continuationToken` that can be used to fetch the next page of + * results from the server. + * + * This overload may be called during standard Activity processing, at which + * point the Bot's service URL and credentials that are part of the current + * activity processing pipeline will be used. + * + * @param context The context object for the turn. + * @return List of Members of the current conversation + */ + public CompletableFuture getConversations(TurnContextImpl context) { + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + return connectorClient.getConversations().getConversations(); + } + + /** + * Lists the Conversations in which this bot has participated for a given + * channel server. The channel server returns results in pages and each page + * will include a `continuationToken` that can be used to fetch the next page of + * results from the server. + * + * This overload may be called during standard Activity processing, at which + * point the Bot's service URL and credentials that are part of the current + * activity processing pipeline will be used. + * + * @param context The context object for the turn. + * @param continuationToken The continuation token from the previous page of + * results. + * @return List of Members of the current conversation + */ + public CompletableFuture getConversations(TurnContextImpl context, String continuationToken) { + ConnectorClient connectorClient = context.getTurnState().get(CONNECTOR_CLIENT_KEY); + return connectorClient.getConversations().getConversations(continuationToken); + } + + /** + * Attempts to retrieve the token for a user that's in a login flow. + * + * @param context Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param magicCode (Optional) Optional user entered code to validate. + * @return Token Response + */ + @Override + public CompletableFuture getUserToken(TurnContext context, String connectionName, String magicCode) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + if (context.getActivity().getFrom() == null || StringUtils.isEmpty(context.getActivity().getFrom().getId())) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.getUserToken(): missing from or from.id") + ); + } + + if (StringUtils.isEmpty(connectionName)) { + return Async.completeExceptionally(new IllegalArgumentException("connectionName")); + } + + return createOAuthAPIClient(context, null).thenCompose( + oAuthClient -> oAuthClient.getUserToken() + .getToken( + context.getActivity().getFrom().getId(), + connectionName, + context.getActivity().getChannelId(), + magicCode + ) + ); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param context Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture getOAuthSignInLink(TurnContext context, String connectionName) { + return getOAuthSignInLink(context, null, connectionName); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param context Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated with the token. + * @param finalRedirect The final URL that the OAuth flow will redirect to. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture getOAuthSignInLink( + TurnContext context, + String connectionName, + String userId, + String finalRedirect + ) { + return getOAuthSignInLink(context, null, connectionName, userId, finalRedirect); + } + + /** + * Signs the user out with the token server. + * + * @param context Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture signOutUser(TurnContext context, String connectionName, String userId) { + return signOutUser(context, null, connectionName, userId); + } + + /** + * Retrieves the token status for each configured connection for the given user. + * + * @param context Context for the current turn of conversation with the + * user. + * @param userId The user Id for which token status is retrieved. + * @param includeFilter Optional comma separated list of connection's to + * include. Blank will return token status for all + * configured connections. + * @return Array of {@link TokenStatus}. + */ + @Override + public CompletableFuture> getTokenStatus( + TurnContext context, + String userId, + String includeFilter + ) { + return getTokenStatus(context, null, userId, includeFilter); + } + + /** + * Retrieves Azure Active Directory tokens for particular resources on a + * configured connection. + * + * @param context Context for the current turn of conversation with the + * user. + * @param connectionName The name of the Azure Active Directory connection + * configured with this bot. + * @param resourceUrls The list of resource URLs to retrieve tokens for. + * @param userId The user Id for which tokens are retrieved. If passing + * in null the userId is taken from the Activity in the + * TurnContext. + * @return Map of resourceUrl to the corresponding {@link TokenResponse}. + */ + @Override + public CompletableFuture> getAadTokens( + TurnContext context, + String connectionName, + String[] resourceUrls, + String userId + ) { + return getAadTokens(context, null, connectionName, resourceUrls, userId); + } + + /** + * Creates a conversation on the specified channel. + * + * To start a conversation, your bot must know its account information and the + * user's account information on that channel. Most channels only support + * initiating a direct message (non-group) conversation. + *

+ * The adapter attempts to create a new conversation on the channel, and then + * sends a {@code conversationUpdate} activity through its middleware pipeline + * to the {@code callback} method. + *

+ *

+ * If the conversation is established with the specified users, the ID of the + * activity's {@link Activity#getConversation} will contain the ID of the new + * conversation. + *

+ * + * @param channelId The ID for the channel. + * @param serviceUrl The channel's service URL endpoint. + * @param credentials The application credentials for the bot. + * @param conversationParameters The conversation information to use to create + * the conversation. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture createConversation( + String channelId, + String serviceUrl, + MicrosoftAppCredentials credentials, + ConversationParameters conversationParameters, + BotCallbackHandler callback + ) { + return getOrCreateConnectorClient(serviceUrl, credentials).thenCompose(connectorClient -> { + Conversations conversations = connectorClient.getConversations(); + return conversations.createConversation(conversationParameters) + .thenCompose(conversationResourceResponse -> { + // Create a event activity to represent the result. + Activity eventActivity = Activity.createEventActivity(); + eventActivity.setName(ActivityEventNames.CREATE_CONVERSATION); + eventActivity.setChannelId(channelId); + eventActivity.setServiceUrl(serviceUrl); + eventActivity.setId( + (conversationResourceResponse.getActivityId() != null) + ? conversationResourceResponse.getActivityId() + : UUID.randomUUID().toString() + ); + eventActivity.setConversation(new ConversationAccount(conversationResourceResponse.getId()) { + { + setTenantId(conversationParameters.getTenantId()); + } + }); + eventActivity.setChannelData(conversationParameters.getChannelData()); + eventActivity.setRecipient(conversationParameters.getBot()); + + // run pipeline + CompletableFuture result = new CompletableFuture<>(); + try (TurnContextImpl context = new TurnContextImpl(this, eventActivity)) { + HashMap claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, credentials.getAppId()); + claims.put(AuthenticationConstants.APPID_CLAIM, credentials.getAppId()); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity claimsIdentity = new ClaimsIdentity("anonymous", claims); + + context.getTurnState().add(BOT_IDENTITY_KEY, claimsIdentity); + context.getTurnState().add(CONNECTOR_CLIENT_KEY, connectorClient); + + result = runPipeline(context, callback); + } catch (Exception e) { + result.completeExceptionally(e); + } + return result; + }); + }); + } + + /** + * Creates a conversation on the specified channel. + * + * To start a conversation, your bot must know its account information and the + * user's account information on that channel. Most channels only support + * initiating a direct message (non-group) conversation. + *

+ * The adapter attempts to create a new conversation on the channel, and then + * sends a {@code conversationUpdate} activity through its middleware pipeline + * to the {@code callback} method. + *

+ *

+ * If the conversation is established with the specified users, the ID of the + * activity's {@link Activity#getConversation} will contain the ID of the new + * conversation. + *

+ * + * @param channelId The ID for the channel. + * @param serviceUrl The channel's service URL endpoint. + * @param credentials The application credentials for the bot. + * @param conversationParameters The conversation information to use to create + * the conversation. + * @param callback The method to call for the resulting bot turn. + * @param reference A conversation reference that contains the + * tenant. + * @return A task that represents the work queued to execute. + */ + @SuppressWarnings("checkstyle:InnerAssignment") + @Deprecated + public CompletableFuture createConversation( + String channelId, + String serviceUrl, + MicrosoftAppCredentials credentials, + ConversationParameters conversationParameters, + BotCallbackHandler callback, + ConversationReference reference + ) { + if (reference.getConversation() == null) { + return CompletableFuture.completedFuture(null); + } + + String tenantId = reference.getConversation().getTenantId(); + if (!StringUtils.isEmpty(tenantId)) { + // Putting tenantId in channelData is a temporary solution while we wait for the + // Teams API to be updated + if (conversationParameters.getChannelData() != null) { + ((ObjectNode) conversationParameters.getChannelData()).set( + "tenantId", + JsonNodeFactory.instance.textNode(tenantId) + ); + } else { + ObjectNode channelData = JsonNodeFactory.instance.objectNode(); + channelData.set( + "tenant", + JsonNodeFactory.instance.objectNode() + .set("tenantId", JsonNodeFactory.instance.textNode(tenantId)) + ); + + conversationParameters.setChannelData(channelData); + } + + conversationParameters.setTenantId(tenantId); + } + + return createConversation(channelId, serviceUrl, credentials, conversationParameters, callback); + } + + /** + * Creates an OAuth client for the bot. + * + *

+ * Note: This is protected primarily so that unit tests can override to provide + * a mock OAuthClient. + *

+ * + * @param turnContext The context object for the current turn. + * @param oAuthAppCredentials The credentials to use when creating the client. + * If null, the default credentials will be used. + * @return An OAuth client for the bot. + */ + protected CompletableFuture createOAuthAPIClient( + TurnContext turnContext, + AppCredentials oAuthAppCredentials + ) { + if ( + !OAuthClientConfig.emulateOAuthCards + && StringUtils.equalsIgnoreCase(turnContext.getActivity().getChannelId(), Channels.EMULATOR) + && credentialProvider.isAuthenticationDisabled().join() + ) { + OAuthClientConfig.emulateOAuthCards = true; + } + AtomicBoolean sendEmulateOAuthCards = new AtomicBoolean(false); + + String appId = getBotAppId(turnContext); + String cacheKey = appId + (oAuthAppCredentials != null ? oAuthAppCredentials.getAppId() : ""); + + OAuthClient client = oAuthClients.computeIfAbsent(cacheKey, key -> { + sendEmulateOAuthCards.set(OAuthClientConfig.emulateOAuthCards); + + String oAuthScope = getBotFrameworkOAuthScope(); + AppCredentials credentials = + oAuthAppCredentials != null ? oAuthAppCredentials : getAppCredentials(appId, oAuthScope).join(); + + return new RestOAuthClient( + OAuthClientConfig.emulateOAuthCards + ? turnContext.getActivity().getServiceUrl() + : OAuthClientConfig.OAUTHENDPOINT, + credentials + ); + }); + + // adding the oAuthClient into the TurnState + if (turnContext.getTurnState().get(BotAdapter.OAUTH_CLIENT_KEY) == null) { + turnContext.getTurnState().add(BotAdapter.OAUTH_CLIENT_KEY, client); + } + + if (sendEmulateOAuthCards.get()) { + return client.getUserToken().sendEmulateOAuthCards(true).thenApply(voidresult -> client); + } + + return CompletableFuture.completedFuture(client); + } + + /** + * Creates the connector client asynchronous. + * + * @param serviceUrl The service URL. + * @param claimsIdentity The claims identity. + * @param audience The target audience for the connector. + * @return ConnectorClient instance. + * @throws UnsupportedOperationException ClaimsIdentity cannot be null. Pass + * Anonymous ClaimsIdentity if + * authentication is turned off. + */ + @SuppressWarnings(value = "PMD") + public CompletableFuture createConnectorClient( + String serviceUrl, + ClaimsIdentity claimsIdentity, + String audience + ) { + if (claimsIdentity == null) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "ClaimsIdentity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off." + ) + ); + } + + // For requests from channel App Id is in Audience claim of JWT token. For + // emulator it is in AppId claim. + // For unauthenticated requests we have anonymous identity provided auth is + // disabled. + if (claimsIdentity.claims() == null) { + return getOrCreateConnectorClient(serviceUrl); + } + + // For Activities coming from Emulator AppId claim contains the Bot's AAD AppId. + // For anonymous requests (requests with no header) appId is not set in claims. + + String botAppIdClaim = claimsIdentity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (botAppIdClaim == null) { + botAppIdClaim = claimsIdentity.claims().get(AuthenticationConstants.APPID_CLAIM); + } + + if (botAppIdClaim != null) { + String scope = audience; + + if (StringUtils.isBlank(audience)) { + scope = SkillValidation.isSkillClaim(claimsIdentity.claims()) + ? String.format("%s/.default", JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())) + : getBotFrameworkOAuthScope(); + } + + return getAppCredentials(botAppIdClaim, scope) + .thenCompose(credentials -> getOrCreateConnectorClient(serviceUrl, credentials)); + } + + return getOrCreateConnectorClient(serviceUrl); + } + + private CompletableFuture getOrCreateConnectorClient(String serviceUrl) { + return getOrCreateConnectorClient(serviceUrl, null); + } + + /** + * Returns a ConnectorClient, either from a cache or newly created. + * + *

+ * Note: This is protected primarily to allow unit tests to override this to + * provide a mock ConnectorClient + *

+ * + * @param serviceUrl The service URL for the client. + * @param usingAppCredentials (Optional) The AppCredentials to use. + * @return A task that will return the ConnectorClient. + */ + protected CompletableFuture getOrCreateConnectorClient( + String serviceUrl, + AppCredentials usingAppCredentials + ) { + CompletableFuture result = new CompletableFuture<>(); + + String clientKey = keyForConnectorClient( + serviceUrl, + usingAppCredentials != null ? usingAppCredentials.getAppId() : null, + usingAppCredentials != null ? usingAppCredentials.oAuthScope() : null + ); + + result.complete(connectorClients.computeIfAbsent(clientKey, key -> { + try { + RestConnectorClient connectorClient; + if (usingAppCredentials != null) { + connectorClient = + new RestConnectorClient(new URI(serviceUrl).toURL().toString(), usingAppCredentials); + } else { + AppCredentials emptyCredentials = channelProvider != null && channelProvider.isGovernment() + ? MicrosoftGovernmentAppCredentials.empty() + : MicrosoftAppCredentials.empty(); + connectorClient = new RestConnectorClient(new URI(serviceUrl).toURL().toString(), emptyCredentials); + } + + if (connectorClientRetryStrategy != null) { + connectorClient.setRestRetryStrategy(connectorClientRetryStrategy); + } + + return connectorClient; + } catch (Throwable t) { + result.completeExceptionally( + new IllegalArgumentException(String.format("Invalid Service URL: %s", serviceUrl), t) + ); + return null; + } + })); + + return result; + } + + /** + * Gets the application credentials. App Credentials are cached so as to ensure + * we are not refreshing token every time. + * + * @param appId The application identifier (AAD Id for the bot). + * @return App credentials. + */ + private CompletableFuture getAppCredentials(String appId, String scope) { + if (appId == null) { + return CompletableFuture.completedFuture(MicrosoftAppCredentials.empty()); + } + + String cacheKey = keyForAppCredentials(appId, scope); + if (appCredentialMap.containsKey(cacheKey)) { + return CompletableFuture.completedFuture(appCredentialMap.get(cacheKey)); + } + + // If app credentials were provided, use them as they are the preferred choice + // moving forward + if (appCredentials != null) { + appCredentialMap.put(cacheKey, appCredentials); + return CompletableFuture.completedFuture(appCredentials); + } + + // Create a new AppCredentials and add it to the cache. + return buildAppCredentials(appId, scope).thenApply(credentials -> { + appCredentialMap.put(cacheKey, credentials); + return credentials; + }); + } + + /** + * Creates an AppCredentials object for the specified appId and scope. + * + * @param appId The appId. + * @param scope The scope. + * @return An AppCredentials object. + */ + protected CompletableFuture buildAppCredentials(String appId, String scope) { + return credentialProvider.getAppPassword(appId).thenApply(appPassword -> { + AppCredentials credentials = channelProvider != null && channelProvider.isGovernment() + ? new MicrosoftGovernmentAppCredentials(appId, appPassword, null, scope) + : new MicrosoftAppCredentials(appId, appPassword, null, scope); + return credentials; + }); + } + + private String getBotAppId(TurnContext turnContext) throws IllegalStateException { + ClaimsIdentity botIdentity = turnContext.getTurnState().get(BOT_IDENTITY_KEY); + if (botIdentity == null) { + throw new IllegalStateException("An IIdentity is required in TurnState for this operation."); + } + + String appId = botIdentity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appId)) { + throw new IllegalStateException("Unable to get the bot AppId from the audience claim."); + } + + return appId; + } + + private String getBotFrameworkOAuthScope() { + return channelProvider != null && channelProvider.isGovernment() + ? GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + : AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE; + } + + /** + * Generates the key for accessing the app credentials cache. + * + * @param appId The appId + * @param scope The scope. + * @return The cache key + */ + protected static String keyForAppCredentials(String appId, String scope) { + return appId + (StringUtils.isEmpty(scope) ? "" : scope); + } + + /** + * Generates the key for accessing the connector client cache. + * + * @param serviceUrl The service url + * @param appId The app did + * @param scope The scope + * @return The cache key. + */ + protected static String keyForConnectorClient(String serviceUrl, String appId, String scope) { + return serviceUrl + (appId == null ? "" : appId) + (scope == null ? "" : scope); + } + + /** + * Middleware to assign tenantId from channelData to Conversation.TenantId. + * + * MS Teams currently sends the tenant ID in channelData and the correct + * behavior is to expose this value in Activity.Conversation.TenantId. + * + * This code copies the tenant ID from channelData to + * Activity.Conversation.TenantId. Once MS Teams sends the tenantId in the + * Conversation property, this middleware can be removed. + */ + private static class TenantIdWorkaroundForTeamsMiddleware implements Middleware { + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + if ( + StringUtils.equalsIgnoreCase(turnContext.getActivity().getChannelId(), Channels.MSTEAMS) + && turnContext.getActivity().getConversation() != null + && StringUtils.isEmpty(turnContext.getActivity().getConversation().getTenantId()) + ) { + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode teamsChannelData = mapper.valueToTree(turnContext.getActivity().getChannelData()); + if ( + teamsChannelData != null && teamsChannelData.has("tenant") + && teamsChannelData.get("tenant").has("id") + ) { + + turnContext.getActivity() + .getConversation() + .setTenantId(teamsChannelData.get("tenant").get("id").asText()); + } + } + + return next.next(); + } + } + + /** + * Get the AppCredentials cache. For unit testing. + * + * @return The AppCredentials cache. + */ + protected Map getCredentialsCache() { + return Collections.unmodifiableMap(appCredentialMap); + } + + /** + * Get the ConnectorClient cache. FOR UNIT TESTING. + * + * @return The ConnectorClient cache. + */ + protected Map getConnectorClientCache() { + return Collections.unmodifiableMap(connectorClients); + } + + /** + * Attempts to retrieve the token for a user that's in a login flow, using + * customized AppCredentials. + * + * @param context Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param magicCode (Optional) Optional user entered code to validate. + * + * @return Token Response. + */ + @Override + public CompletableFuture getUserToken( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName, + String magicCode + ) { + + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + + if (context.getActivity().getFrom() == null || StringUtils.isEmpty(context.getActivity().getFrom().getId())) { + return Async.completeExceptionally( + new IllegalArgumentException("BotFrameworkAdapter.GetUserTokenAsync(): missing from or from.id") + ); + } + + if (StringUtils.isEmpty(connectionName)) { + return Async.completeExceptionally(new IllegalArgumentException("connectionName cannot be null.")); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(client -> { + return client.getUserToken() + .getToken( + context.getActivity().getFrom().getId(), + connectionName, + context.getActivity().getChannelId(), + magicCode + ); + }); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name, using customized AppCredentials. + * + * @param context Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * + * @return A task that represents the work queued to execute. + * + * If the task completes successfully, the result contains the raw + * signin link. + */ + @Override + public CompletableFuture getOAuthSignInLink( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName + ) { + + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + + if (StringUtils.isEmpty(connectionName)) { + Async.completeExceptionally(new IllegalArgumentException("connectionName cannot be null.")); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(oAuthClient -> { + try { + Activity activity = context.getActivity(); + String appId = getBotAppId(context); + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setActivityId(activity.getId()); + conversationReference.setBot(activity.getRecipient()); + conversationReference.setChannelId(activity.getChannelId()); + conversationReference.setConversation(activity.getConversation()); + conversationReference.setServiceUrl(activity.getServiceUrl()); + conversationReference.setUser(activity.getFrom()); + + TokenExchangeState tokenExchangeState = new TokenExchangeState(); + tokenExchangeState.setConnectionName(connectionName); + tokenExchangeState.setConversation(conversationReference); + tokenExchangeState.setRelatesTo(activity.getRelatesTo()); + tokenExchangeState.setMsAppId(appId); + + String serializedState = Serialization.toString(tokenExchangeState); + String state = Base64.getEncoder().encodeToString(serializedState.getBytes(StandardCharsets.UTF_8)); + + return oAuthClient.getBotSignIn().getSignInUrl(state); + } catch (Throwable t) { + throw new CompletionException(t); + } + }); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name, using the bot's AppCredentials. + * + * @param context Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated with the token. + * @param finalRedirect The final URL that the OAuth flow will redirect to. + * + * @return A task that represents the work queued to execute. + * + * If the task completes successfully, the result contains the raw + * signin link. + */ + @Override + public CompletableFuture getOAuthSignInLink( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId, + String finalRedirect + ) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + if (StringUtils.isEmpty(connectionName)) { + return Async.completeExceptionally(new IllegalArgumentException("connectionName")); + } + if (StringUtils.isEmpty(userId)) { + return Async.completeExceptionally(new IllegalArgumentException("userId")); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(oAuthClient -> { + try { + Activity activity = context.getActivity(); + String appId = getBotAppId(context); + + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setActivityId(activity.getId()); + conversationReference.setBot(activity.getRecipient()); + conversationReference.setChannelId(activity.getChannelId()); + conversationReference.setConversation(activity.getConversation()); + conversationReference.setLocale(activity.getLocale()); + conversationReference.setServiceUrl(activity.getServiceUrl()); + conversationReference.setUser(activity.getFrom()); + TokenExchangeState tokenExchangeState = new TokenExchangeState(); + tokenExchangeState.setConnectionName(connectionName); + tokenExchangeState.setConversation(conversationReference); + tokenExchangeState.setRelatesTo(activity.getRelatesTo()); + tokenExchangeState.setMsAppId(appId); + + String serializedState = Serialization.toString(tokenExchangeState); + String state = Base64.getEncoder().encodeToString(serializedState.getBytes(StandardCharsets.UTF_8)); + + return oAuthClient.getBotSignIn().getSignInUrl(state, null, null, finalRedirect); + } catch (Throwable t) { + throw new CompletionException(t); + } + }); + } + + /** + * Signs the user out with the token server, using customized AppCredentials. + * + * @param context Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId User id of user to sign out. + * + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture signOutUser( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId + ) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + if (StringUtils.isEmpty(connectionName)) { + return Async.completeExceptionally(new IllegalArgumentException("connectionName")); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(oAuthClient -> { + return oAuthClient.getUserToken() + .signOut(context.getActivity().getFrom().getId(), connectionName, context.getActivity().getChannelId()); + }).thenApply(signOutResult -> null); + } + + /** + * Retrieves the token status for each configured connection for the given user, + * using customized AppCredentials. + * + * @param context Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param userId The user Id for which token status is retrieved. + * @param includeFilter Optional comma separated list of connection's to + * include. Blank will return token status for all + * configured connections. + * + * @return List of TokenStatus. + */ + @Override + public CompletableFuture> getTokenStatus( + TurnContext context, + AppCredentials oAuthAppCredentials, + String userId, + String includeFilter + ) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + if (StringUtils.isEmpty(userId)) { + return Async.completeExceptionally(new IllegalArgumentException("userId")); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(oAuthClient -> { + return oAuthClient.getUserToken() + .getTokenStatus(userId, context.getActivity().getChannelId(), includeFilter); + }); + } + + /** + * Retrieves Azure Active Directory tokens for particular resources on a + * configured connection, using customized AppCredentials. + * + * @param context Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName The name of the Azure Active Directory connection + * configured with this bot. + * @param resourceUrls The list of resource URLs to retrieve tokens for. + * @param userId The user Id for which tokens are retrieved. If + * passing in null the userId is taken from the + * Activity in the TurnContext. + * + * @return Dictionary of resourceUrl to the corresponding TokenResponse. + */ + @Override + public CompletableFuture> getAadTokens( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName, + String[] resourceUrls, + String userId + ) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + if (StringUtils.isEmpty(connectionName)) { + return Async.completeExceptionally(new IllegalArgumentException("connectionName")); + } + if (resourceUrls == null) { + return Async.completeExceptionally(new IllegalArgumentException("resourceUrls")); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(oAuthClient -> { + String effectiveUserId = userId; + if ( + StringUtils.isEmpty(effectiveUserId) && context.getActivity() != null + && context.getActivity().getFrom() != null + ) { + effectiveUserId = context.getActivity().getFrom().getId(); + } + + return oAuthClient.getUserToken() + .getAadTokens(effectiveUserId, connectionName, new AadResourceUrls(resourceUrls)); + }); + + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * + * @return A task that represents the work queued to execute. + * + * If the task completes successfully, the result contains the raw + * signin link. + */ + @Override + public CompletableFuture getSignInResource(TurnContext turnContext, String connectionName) { + return getSignInResource(turnContext, connectionName, turnContext.getActivity().getFrom().getId(), null); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated with the token. + * @param finalRedirect The final URL that the OAuth flow will redirect to. + * + * @return A task that represents the work queued to execute. + * + * If the task completes successfully, the result contains the raw + * signin link. + */ + @Override + public CompletableFuture getSignInResource( + TurnContext turnContext, + String connectionName, + String userId, + String finalRedirect + ) { + return getSignInResource(turnContext, null, connectionName, userId, finalRedirect); + } + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param context Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated with the + * token. + * @param finalRedirect The final URL that the OAuth flow will redirect + * to. + * + * @return A task that represents the work queued to execute. + * + * If the task completes successfully, the result contains the raw + * signin link. + */ + @Override + public CompletableFuture getSignInResource( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId, + String finalRedirect + ) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + + if (StringUtils.isEmpty(connectionName)) { + throw new IllegalArgumentException("connectionName cannot be null."); + } + + if (StringUtils.isEmpty(userId)) { + throw new IllegalArgumentException("userId cannot be null."); + } + + return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(oAuthClient -> { + try { + Activity activity = context.getActivity(); + String appId = getBotAppId(context); + + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setActivityId(activity.getId()); + conversationReference.setBot(activity.getRecipient()); + conversationReference.setChannelId(activity.getChannelId()); + conversationReference.setConversation(activity.getConversation()); + conversationReference.setLocale(activity.getLocale()); + conversationReference.setServiceUrl(activity.getServiceUrl()); + conversationReference.setUser(activity.getFrom()); + TokenExchangeState tokenExchangeState = new TokenExchangeState(); + tokenExchangeState.setConnectionName(connectionName); + tokenExchangeState.setConversation(conversationReference); + tokenExchangeState.setRelatesTo(activity.getRelatesTo()); + tokenExchangeState.setMsAppId(appId); + + String serializedState = Serialization.toString(tokenExchangeState); + String state = Base64.getEncoder().encodeToString(serializedState.getBytes(StandardCharsets.UTF_8)); + + return oAuthClient.getBotSignIn().getSignInResource(state, null, null, finalRedirect); + } catch (Throwable t) { + throw new CompletionException(t); + } + }); + + } + + /** + * Performs a token exchange operation such as for single sign-on. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id associated with the token.. + * @param exchangeRequest The exchange request details, either a token to + * exchange or a uri to exchange. + * + * @return If the task completes, the exchanged token is returned. + */ + @Override + public CompletableFuture exchangeToken( + TurnContext turnContext, + String connectionName, + String userId, + TokenExchangeRequest exchangeRequest + ) { + return exchangeToken(turnContext, null, connectionName, userId, exchangeRequest); + } + + /** + * Performs a token exchange operation such as for single sign-on. + * + * @param turnContext Context for the current turn of conversation with + * the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId The user id associated with the token.. + * @param exchangeRequest The exchange request details, either a token to + * exchange or a uri to exchange. + * + * @return If the task completes, the exchanged token is returned. + */ + @Override + public CompletableFuture exchangeToken( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId, + TokenExchangeRequest exchangeRequest + ) { + + if (StringUtils.isEmpty(connectionName)) { + return Async.completeExceptionally(new IllegalArgumentException("connectionName is null or empty")); + } + + if (StringUtils.isEmpty(userId)) { + return Async.completeExceptionally(new IllegalArgumentException("userId is null or empty")); + } + + if (exchangeRequest == null) { + return Async.completeExceptionally(new IllegalArgumentException("exchangeRequest is null")); + } + + if (StringUtils.isEmpty(exchangeRequest.getToken()) && StringUtils.isEmpty(exchangeRequest.getUri())) { + return Async.completeExceptionally( + new IllegalArgumentException("Either a Token or Uri property is required on the TokenExchangeRequest") + ); + } + + return createOAuthAPIClient(turnContext, oAuthAppCredentials).thenCompose(oAuthClient -> { + return oAuthClient.getUserToken() + .exchangeToken(userId, connectionName, turnContext.getActivity().getChannelId(), exchangeRequest); + + }); + } + + /** + * Inserts a ConnectorClient into the cache. FOR UNIT TESTING ONLY. + * + * @param serviceUrl The service url + * @param appId The app did + * @param scope The scope + * @param client The ConnectorClient to insert. + */ + protected void addConnectorClientToCache(String serviceUrl, String appId, String scope, ConnectorClient client) { + String key = BotFrameworkAdapter.keyForConnectorClient(serviceUrl, appId, scope); + connectorClients.put(key, client); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java new file mode 100644 index 000000000..a48e84195 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.connector.Async; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Defines a state management object and automates the reading and writing of + * associated state properties to a storage layer. + * + *

+ * Each state management object defines a scope for a storage layer. State + * properties are created within a state management scope, and the Bot Framework + * defines these scopes: {@link ConversationState}, {@link UserState}, and + * {@link PrivateConversationState}. You can define additional scopes for your + * bot. + *

+ */ +public abstract class BotState implements PropertyManager { + /** + * The key for the state cache. + */ + private String contextServiceKey; + + /** + * The storage layer this state management object will use. + */ + private Storage storage; + + private ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + /** + * Initializes a new instance of the BotState class. + * + * @param withStorage The storage provider to use. + * @param withContextServiceKey The key for the state cache for this BotState. + * @throws IllegalArgumentException Null Storage or empty service key arguments. + */ + public BotState(Storage withStorage, String withContextServiceKey) throws IllegalArgumentException { + if (withStorage == null) { + throw new IllegalArgumentException("Storage cannot be null"); + } + storage = withStorage; + + if (StringUtils.isEmpty(withContextServiceKey)) { + throw new IllegalArgumentException("context service key cannot be empty"); + } + contextServiceKey = withContextServiceKey; + } + + /** + * Creates a named state property within the scope of a BotState and returns an + * accessor for the property. + * + * @param name name of property. + * @param type of property. + * @return A {@link StatePropertyAccessor} for the property. + * @throws IllegalArgumentException Empty name + */ + public StatePropertyAccessor createProperty(String name) throws IllegalArgumentException { + if (StringUtils.isEmpty(name)) { + throw new IllegalArgumentException("name cannot be empty"); + } + + return new BotStatePropertyAccessor<>(this, name); + } + + /** + * Populates the state cache for this BotState from the storage layer. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture load(TurnContext turnContext) { + return load(turnContext, false); + } + + /** + * Reads in the current state object and caches it in the context object for + * this turn. + * + * @param turnContext The context object for this turn. + * @param force true to overwrite any existing state cache; or false to + * load state from storage only if the cache doesn't already + * exist. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture load(TurnContext turnContext, boolean force) { + return Async.tryCompletable(() -> { + if (turnContext == null) { + throw new IllegalArgumentException("turnContext cannot be null"); + } + + CachedBotState cachedState = turnContext.getTurnState().get(contextServiceKey); + String storageKey = getStorageKey(turnContext); + if (force || cachedState == null || cachedState.getState() == null) { + return storage.read(new String[]{storageKey}).thenApply(val -> { + turnContext.getTurnState() + .replace( + contextServiceKey, + new CachedBotState((Map) val.get(storageKey)) + ); + return null; + }); + } + + return CompletableFuture.completedFuture(null); + }); + } + + /** + * Writes the state cache for this BotState to the storage layer. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture saveChanges(TurnContext turnContext) { + return saveChanges(turnContext, false); + } + + /** + * Writes the state cache for this BotState to the storage layer. + * + * @param turnContext The context object for this turn. + * @param force true to save the state cache to storage; or false to save + * state to storage only if a property in the cache has + * changed. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture saveChanges(TurnContext turnContext, boolean force) { + return Async.tryCompletable(() -> { + if (turnContext == null) { + throw new IllegalArgumentException("turnContext cannot be null"); + } + + CachedBotState cachedState = turnContext.getTurnState().get(contextServiceKey); + if (force || cachedState != null && cachedState.isChanged()) { + String storageKey = getStorageKey(turnContext); + Map changes = new HashMap(); + changes.put(storageKey, cachedState.state); + + return storage.write(changes).thenApply(val -> { + cachedState.setHash(cachedState.computeHash(cachedState.state)); + return null; + }); + } + + return CompletableFuture.completedFuture(null); + }); + } + + /** + * Clears the state cache for this BotState. + * + *

+ * This method clears the state cache in the turn context. Call + * {@link #saveChanges(TurnContext, boolean)} to persist this change in the + * storage layer. + *

+ * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture clearState(TurnContext turnContext) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "TurnContext cannot be null." + )); + } + + turnContext.getTurnState().replace(contextServiceKey, new CachedBotState()); + return CompletableFuture.completedFuture(null); + } + + /** + * Delete any state currently stored in this state scope. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture delete(TurnContext turnContext) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "TurnContext cannot be null." + )); + } + + String storageKey = getStorageKey(turnContext); + return storage.delete(new String[] {storageKey}).thenApply(result -> { + CachedBotState cachedState = turnContext.getTurnState().get(contextServiceKey); + if (cachedState != null) { + turnContext.getTurnState().remove(contextServiceKey); + } + + return null; + }); + } + + /** + * Gets a copy of the raw cached data for this BotState from the turn context. + * + * @param turnContext The context object for this turn. + * @return A JSON representation of the cached state. + */ + public JsonNode get(TurnContext turnContext) { + if (turnContext == null) { + throw new IllegalArgumentException("turnContext cannot be null"); + } + + String stateKey = getClass().getSimpleName(); + CachedBotState cachedState = turnContext.getTurnState().get(stateKey); + return mapper.valueToTree(cachedState.state); + } + + /** + * Gets the cached bot state instance that wraps the raw cached data for this BotState from the turn context. + * + * @param turnContext The context object for this turn. + * @return The cached bot state instance. + */ + public CachedBotState getCachedState(TurnContext turnContext) { + if (turnContext == null) { + throw new IllegalArgumentException("turnContext cannot be null"); + } + + return turnContext.getTurnState().get(contextServiceKey); + } + + /** + * When overridden in a derived class, gets the key to use when reading and + * writing state to and from storage. + * + * @param turnContext The context object for this turn. + * @return The storage key. + * @throws IllegalArgumentException TurnContext doesn't contain all the required data. + */ + public abstract String getStorageKey(TurnContext turnContext) throws IllegalArgumentException; + + /** + * Gets the value of a property from the state cache for this BotState. + * + * @param turnContext The context object for this turn. + * @param propertyName The name of the property to get. + * @param The property type. + * @return A task that represents the work queued to execute. If the task is + * successful, the result contains the property value. + */ + protected CompletableFuture getPropertyValue( + TurnContext turnContext, + String propertyName + ) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (StringUtils.isEmpty(propertyName)) { + return Async.completeExceptionally(new IllegalArgumentException( + "propertyName cannot be empty" + )); + } + + return Async.tryCompletable(() -> { + CachedBotState cachedState = turnContext.getTurnState().get(contextServiceKey); + return (CompletableFuture) CompletableFuture + .completedFuture(cachedState.getState().get(propertyName)); + }); + } + + /** + * Deletes a property from the state cache for this BotState. + * + * @param turnContext The context object for this turn. + * @param propertyName The name of the property to delete. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture deletePropertyValue( + TurnContext turnContext, + String propertyName + ) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "TurnContext cannot be null." + )); + } + + if (StringUtils.isEmpty(propertyName)) { + return Async.completeExceptionally(new IllegalArgumentException( + "propertyName cannot be empty" + )); + } + + CachedBotState cachedState = turnContext.getTurnState().get(contextServiceKey); + cachedState.getState().remove(propertyName); + return CompletableFuture.completedFuture(null); + } + + /** + * Sets the value of a property in the state cache for this BotState. + * + * @param turnContext The context object for this turn. + * @param propertyName The name of the property to set. + * @param value The value to set on the property. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture setPropertyValue( + TurnContext turnContext, + String propertyName, + Object value + ) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null." + )); + } + + if (StringUtils.isEmpty(propertyName)) { + return Async.completeExceptionally(new IllegalArgumentException( + "propertyName cannot be empty" + )); + } + + CachedBotState cachedState = turnContext.getTurnState().get(contextServiceKey); + cachedState.getState().put(propertyName, value); + return CompletableFuture.completedFuture(null); + } + + /** + * Internal cached bot state. + */ + public static class CachedBotState { + /** + * In memory cache of BotState properties. + */ + private Map state; + + /** + * Used to compute the hash of the state. + */ + private String hash; + + /** + * Object-JsonNode converter. + */ + private ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + /** + * Construct with empty state. + */ + CachedBotState() { + this(null); + } + + /** + * Construct with supplied state. + * + * @param withState The initial state. + */ + CachedBotState(Map withState) { + state = withState != null ? withState : new ConcurrentHashMap<>(); + hash = computeHash(withState); + } + + /** + * @return The Map of key value pairs which are the state. + */ + public Map getState() { + return state; + } + + /** + * @param withState The key value pairs to set the state with. + */ + void setState(Map withState) { + state = withState; + } + + /** + * @return The hash value for the state. + */ + String getHash() { + return hash; + } + + /** + * @param witHashCode Set the hash value. + */ + void setHash(String witHashCode) { + hash = witHashCode; + } + + /** + * + * @return Boolean to tell if the state has changed. + */ + boolean isChanged() { + return !StringUtils.equals(hash, computeHash(state)); + } + + /** + * @param obj The object to compute the hash for. + * @return The computed has for the provided object. + */ + String computeHash(Object obj) { + if (obj == null) { + return ""; + } + + try { + return mapper.writeValueAsString(obj); + } catch (JsonProcessingException e) { + return null; + } + } + } + + /** + * Implements StatePropertyAccessor for an PropertyContainer. + * + *

+ * Note the semantic of this accessor are intended to be lazy, this means teh + * Get, Set and Delete methods will first call LoadAsync. This will be a no-op + * if the data is already loaded. The implication is you can just use this + * accessor in the application code directly without first calling LoadAsync + * this approach works with the AutoSaveStateMiddleware which will save as + * needed at the end of a turn. + *

+ * + * @param type of value the propertyAccessor accesses. + */ + private static class BotStatePropertyAccessor implements StatePropertyAccessor { + /** + * The name of the property. + */ + private String name; + + /** + * The parent BotState. + */ + private BotState botState; + + /** + * StatePropertyAccessor constructor. + * + * @param withState The parent BotState. + * @param withName The property name. + */ + BotStatePropertyAccessor(BotState withState, String withName) { + botState = withState; + name = withName; + } + + /** + * Get the property value. The semantics are intended to be lazy, note the use + * of {@link BotState#load(TurnContext)} at the start. + * + * @param turnContext The context object for this turn. + * @param defaultValueFactory Defines the default value. Invoked when no value + * been set for the requested state property. If + * defaultValueFactory is defined as null, the + * MissingMemberException will be thrown if the + * underlying property is not set. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture get(TurnContext turnContext, Supplier defaultValueFactory) { + return botState.load(turnContext) + .thenCombine(botState.getPropertyValue(turnContext, name), (loadResult, value) -> { + if (value != null) { + return (T) value; + } + + if (defaultValueFactory == null) { + return null; + } + + value = defaultValueFactory.get(); + set(turnContext, (T) value).join(); + return (T) value; + }); + } + + /** + * Delete the property. The semantics are intended to be lazy, note the use of + * {@link BotState#load(TurnContext)} at the start. + * + * @param turnContext The turn context. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture delete(TurnContext turnContext) { + return botState.load(turnContext) + .thenCompose(state -> botState.deletePropertyValue(turnContext, name)); + } + + /** + * Set the property value. The semantics are intended to be lazy, note the use + * of {@link BotState#load(TurnContext)} at the start. + * + * @param turnContext The turn context. + * @param value The value to set. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture set(TurnContext turnContext, T value) { + return botState.load(turnContext) + .thenCompose(state -> botState.setPropertyValue(turnContext, name, value)); + } + + /** + * Gets name of the property. + * + * @return Name of the property. + */ + @Override + public String getName() { + return name; + } + + /** + * Sets name of the property. + * + * @param withName Name of the property. + */ + public void setName(String withName) { + name = withName; + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotStateSet.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotStateSet.java new file mode 100644 index 000000000..7b04e6771 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotStateSet.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Manages a collection of botState and provides ability to load and save in + * parallel. + */ +public class BotStateSet { + /** + * List of BotStates managed by this BotStateSet. + */ + private List botStates = new ArrayList<>(); + + /** + * Initializes a new instance of the BotStateSet class. + * + * @param withBotStates vArgs list of {@link BotState} objects to manage. + */ + public BotStateSet(BotState... withBotStates) { + this(Arrays.asList(withBotStates)); + } + + /** + * Initializes a new instance of the BotStateSet class. + * + * @param withBotStates initial list of {@link BotState} objects to manage. + */ + public BotStateSet(List withBotStates) { + botStates.addAll(withBotStates); + } + + /** + * Gets the BotStates list for the BotStateSet. + * + * @return The BotState objects managed by this class. + */ + public List getBotStates() { + return botStates; + } + + /** + * Sets the BotStates list for the BotStateSet. + * + * @param withBotState The BotState objects managed by this class. + */ + public void setBotStates(List withBotState) { + botStates = withBotState; + } + + /** + * Adds a bot state object to the set. + * + * @param botState The bot state object to add. + * @return The updated BotStateSet, so you can fluently call add(BotState) + * multiple times. + */ + public BotStateSet add(BotState botState) { + if (botState == null) { + throw new IllegalArgumentException("botState"); + } + + botStates.add(botState); + return this; + } + + /** + * Load all BotState records in parallel. + * + * @param turnContext The TurnContext. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture loadAll(TurnContext turnContext) { + return loadAll(turnContext, false); + } + + /** + * Load all BotState records in parallel. + * + * @param turnContext The TurnContext. + * @param force should data be forced into cache. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture loadAll(TurnContext turnContext, boolean force) { + return CompletableFuture.allOf( + botStates.stream().map(future -> future.load(turnContext, force)).toArray(CompletableFuture[]::new) + ); + } + + /** + * Save All BotState changes in parallel. + * + * @param turnContext The TurnContext. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture saveAllChanges(TurnContext turnContext) { + return saveAllChanges(turnContext, false); + } + + /** + * Save All BotState changes in parallel. + * + * @param turnContext The TurnContext. + * @param force should data be forced to save even if no change were + * detected. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture saveAllChanges(TurnContext turnContext, boolean force) { + CompletableFuture[] allSaves = botStates.stream() + .map(botState -> botState.saveChanges(turnContext, force)) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(allSaves); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotTelemetryClient.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotTelemetryClient.java new file mode 100644 index 000000000..9b0ace749 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotTelemetryClient.java @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Map; + +/** + * Logging client for Bot Telemetry. + */ +public interface BotTelemetryClient { + + /** + * Send information about availability of an application. + * + * @param name Availability test name. + * @param timeStamp The time when the availability was captured. + * @param duration The time taken for the availability test to run. + * @param runLocation Name of the location the availability test was run from. + * @param success True if the availability test ran successfully. + */ + default void trackAvailability( + String name, + OffsetDateTime timeStamp, + Duration duration, + String runLocation, + boolean success + ) { + trackAvailability(name, timeStamp, duration, runLocation, success, null, null, null); + } + + /** + * Send information about availability of an application. + * + * @param name Availability test name. + * @param timeStamp The time when the availability was captured. + * @param duration The time taken for the availability test to run. + * @param runLocation Name of the location the availability test was run from. + * @param success True if the availability test ran successfully. + * @param message Error message on availability test run failure. + * @param properties Named string values you can use to classify and search for + * this availability telemetry. + * @param metrics Additional values associated with this availability + * telemetry. + */ + @SuppressWarnings("checkstyle:ParameterNumber") + void trackAvailability( + String name, + OffsetDateTime timeStamp, + Duration duration, + String runLocation, + boolean success, + String message, + Map properties, + Map metrics + ); + + /** + * Send information about an external dependency (outgoing call) in the + * application. + * + * @param dependencyTypeName Name of the command initiated with this dependency + * call. Low cardinality value. Examples are SQL, + * Azure table, and HTTP. + * @param target External dependency target. + * @param dependencyName Name of the command initiated with this dependency + * call. Low cardinality value. Examples are stored + * procedure name and URL path template. + * @param data Command initiated by this dependency call. Examples + * are SQL statement and HTTP URL's with all query + * parameters. + * @param startTime The time when the dependency was called. + * @param duration The time taken by the external dependency to handle + * the call. + * @param resultCode Result code of dependency call execution. + * @param success True if the dependency call was handled + * successfully. + */ + @SuppressWarnings("checkstyle:ParameterNumber") + void trackDependency( + String dependencyTypeName, + String target, + String dependencyName, + String data, + OffsetDateTime startTime, + Duration duration, + String resultCode, + boolean success + ); + + /** + * Logs custom events with extensible named fields. + * + * @param eventName A name for the event. + */ + default void trackEvent(String eventName) { + trackEvent(eventName, null, null); + } + + /** + * Logs custom events with extensible named fields. + * + * @param eventName A name for the event. + * @param properties Named string values you can use to search and classify + * events. + */ + default void trackEvent(String eventName, Map properties) { + trackEvent(eventName, properties, null); + } + + /** + * Logs custom events with extensible named fields. + * + * @param eventName A name for the event. + * @param properties Named string values you can use to search and classify + * events. + * @param metrics Measurements associated with this event. + */ + void trackEvent(String eventName, Map properties, Map metrics); + + /** + * Logs a system exception. + * + * @param exception The exception to log. + */ + default void trackException(Exception exception) { + trackException(exception, null, null); + } + + /** + * Logs a system exception. + * + * @param exception The exception to log. + * @param properties Named string values you can use to classify and search for + * this exception. + * @param metrics Additional values associated with this exception. + */ + void trackException( + Exception exception, + Map properties, + Map metrics + ); + + /** + * Send a trace message. + * + * @param message Message to display. + * @param severityLevel Trace severity level. + * @param properties Named string values you can use to search and classify + * events. + */ + void trackTrace(String message, Severity severityLevel, Map properties); + + /** + * Log a DialogView using the TrackPageView method on the IBotTelemetryClient if + * IBotPageViewTelemetryClient has been implemented. Alternatively log the information out via + * TrackTrace. + * + * @param dialogName The name of the dialog to log the entry / start for. + * @param properties Named string values you can use to search and classify + * events. + * @param metrics Measurements associated with this event. + */ + void trackDialogView(String dialogName, Map properties, Map metrics); + + /** + * Flushes the in-memory buffer and any metrics being pre-aggregated. + */ + void flush(); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java new file mode 100644 index 000000000..14a3cde5d --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java @@ -0,0 +1,632 @@ +package com.microsoft.bot.builder; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.AuthenticationException; +import com.microsoft.bot.connector.authentication.ChannelProvider; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.CredentialProvider; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.connector.authentication.SkillValidation; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.AttachmentData; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.PagedMembersResult; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Transcript; + +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; + +/** + * A class to help with the implementation of the Bot Framework protocol. + */ +public class ChannelServiceHandler { + + private ChannelProvider channelProvider; + + private final AuthenticationConfiguration authConfiguration; + private final CredentialProvider credentialProvider; + + /** + * Initializes a new instance of the {@link ChannelServiceHandler} class, + * using a credential provider. + * + * @param credentialProvider The credential provider. + * @param authConfiguration The authentication configuration. + * @param channelProvider The channel provider. + */ + public ChannelServiceHandler( + CredentialProvider credentialProvider, + AuthenticationConfiguration authConfiguration, + ChannelProvider channelProvider) { + + if (credentialProvider == null) { + throw new IllegalArgumentException("credentialprovider cannot be null"); + } + + if (authConfiguration == null) { + throw new IllegalArgumentException("authConfiguration cannot be null"); + } + + this.credentialProvider = credentialProvider; + this.authConfiguration = authConfiguration; + this.channelProvider = channelProvider; + } + + /** + * Sends an activity to the end of a conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activity The activity to send. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleSendToConversation( + String authHeader, + String conversationId, + Activity activity) { + + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onSendToConversation(claimsIdentity, conversationId, activity); + }); + } + + /** + * Sends a reply to an activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id the reply is to. + * @param activity The activity to send. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleReplyToActivity( + String authHeader, + String conversationId, + String activityId, + Activity activity) { + + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onReplyToActivity(claimsIdentity, conversationId, activityId, activity); + }); + } + + /** + * Edits a previously sent existing activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id to update. + * @param activity The replacement activity. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleUpdateActivity( + String authHeader, + String conversationId, + String activityId, + Activity activity) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onUpdateActivity(claimsIdentity, conversationId, activityId, activity); + }); + } + + /** + * Deletes an existing activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id. + * + * @return A {@link CompletableFuture} representing the result of + * the asynchronous operation. + */ + public CompletableFuture handleDeleteActivity(String authHeader, String conversationId, String activityId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onDeleteActivity(claimsIdentity, conversationId, activityId); + }); + } + + /** + * Enumerates the members of an activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture> handleGetActivityMembers( + String authHeader, + String conversationId, + String activityId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetActivityMembers(claimsIdentity, conversationId, activityId); + }); + } + + /** + * Create a new Conversation. + * + * @param authHeader The authentication header. + * @param parameters Parameters to create the conversation from. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleCreateConversation( + String authHeader, + ConversationParameters parameters) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onCreateConversation(claimsIdentity, parameters); + }); + } + + /** + * Lists the Conversations in which the bot has participated. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param continuationToken A skip or continuation token. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleGetConversations( + String authHeader, + String conversationId, + String continuationToken) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetConversations(claimsIdentity, conversationId, continuationToken); + }); + } + + /** + * Enumerates the members of a conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture> handleGetConversationMembers( + String authHeader, + String conversationId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetConversationMembers(claimsIdentity, conversationId); + }); + } + + /** + * Enumerates the members of a conversation one page at a time. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param pageSize Suggested page size. + * @param continuationToken A continuation token. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleGetConversationPagedMembers( + String authHeader, + String conversationId, + Integer pageSize, + String continuationToken) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetConversationPagedMembers(claimsIdentity, conversationId, pageSize, continuationToken); + }); + } + + /** + * Deletes a member from a conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param memberId Id of the member to delete from this + * conversation. + * + * @return A {@link CompletableFuture} representing the + * asynchronous operation. + */ + public CompletableFuture handleDeleteConversationMember( + String authHeader, + String conversationId, + String memberId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onDeleteConversationMember(claimsIdentity, conversationId, memberId); + }); + } + + /** + * Uploads the historic activities of the conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param transcript Transcript of activities. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleSendConversationHistory( + String authHeader, + String conversationId, + Transcript transcript) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onSendConversationHistory(claimsIdentity, conversationId, transcript); + }); + } + + /** + * Stores data in a compliant store when dealing with enterprises. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param attachmentUpload Attachment data. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleUploadAttachment( + String authHeader, + String conversationId, + AttachmentData attachmentUpload) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onUploadAttachment(claimsIdentity, conversationId, attachmentUpload); + }); + } + + /** + * SendToConversation() API for Skill. + * + * This method allows you to send an activity to the end of a conversation. + * This is slightly different from ReplyToActivity(). * + * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the + * channel. * ReplyToActivity(conversationId,ActivityId) - adds the + * activity as a reply to another activity, if the channel supports it. If + * the channel does not support nested replies, ReplyToActivity falls back + * to SendToConversation. Use ReplyToActivity when replying to a specific + * activity in the conversation. Use SendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param activity Activity to send. + * + * @return task for a resource response. + */ + protected CompletableFuture onSendToConversation( + ClaimsIdentity claimsIdentity, + String conversationId, + Activity activity) { + throw new NotImplementedException("onSendToConversation is not implemented"); + } + + /** + * OnReplyToActivity() API. + * + * Override this method allows to reply to an Activity. This is slightly + * different from SendToConversation(). * + * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the + * channel. * ReplyToActivity(conversationId,ActivityId) - adds the + * activity as a reply to another activity, if the channel supports it. If + * the channel does not support nested replies, ReplyToActivity falls back + * to SendToConversation. Use ReplyToActivity when replying to a specific + * activity in the conversation. Use SendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId activityId the reply is to (OPTONAL). + * @param activity Activity to send. + * + * @return task for a resource response. + */ + protected CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + throw new NotImplementedException("onReplyToActivity is not implemented"); + } + + /** + * OnUpdateActivity() API. + * + * Override this method to edit a previously sent existing activity. Some + * channels allow you to edit an existing activity to reflect the new state + * of a bot conversation. For example, you can remove buttons after someone + * has clicked "Approve" button. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId activityId to update. + * @param activity replacement Activity. + * + * @return task for a resource response. + */ + protected CompletableFuture onUpdateActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + throw new NotImplementedException("onUpdateActivity is not implemented"); + } + + /** + * OnDeleteActivity() API. + * + * Override this method to Delete an existing activity. Some channels allow + * you to delete an existing activity, and if successful this method will + * remove the specified activity. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId activityId to delete. + * + * @return task for a resource response. + */ + protected CompletableFuture onDeleteActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + throw new NotImplementedException("onDeleteActivity is not implemented"); + } + + /** + * OnGetActivityMembers() API. + * + * Override this method to enumerate the members of an activity. This REST + * API takes a ConversationId and a ActivityId, returning an array of + * ChannelAccount Objects representing the members of the particular + * activity in the conversation. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId Activity D. + * + * @return task with result. + */ + protected CompletableFuture> onGetActivityMembers( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + throw new NotImplementedException("onGetActivityMembers is not implemented"); + } + + /** + * CreateConversation() API. + * + * Override this method to create a new Conversation. POST to this method + * with a * Bot being the bot creating the conversation * IsGroup set to + * true if this is not a direct message (default instanceof false) * Array + * containing the members to include in the conversation The return value + * is a ResourceResponse which contains a conversation D which is suitable + * for use in the message payload and REST API URIs. Most channels only + * support the semantics of bots initiating a direct message conversation. + * An example of how to do that would be: var resource = + * connector.getconversations().CreateConversation(new + * ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { + * new ChannelAccount("user1") } ); + * connect.getConversations().OnSendToConversation(resource.getId(), new + * Activity() ... ) ; end. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param parameters Parameters to create the conversation + * from. + * + * @return task for a conversation resource response. + */ + protected CompletableFuture onCreateConversation( + ClaimsIdentity claimsIdentity, + ConversationParameters parameters) { + throw new NotImplementedException("onCreateConversation is not implemented"); + } + + /** + * OnGetConversations() API for Skill. + * + * Override this method to list the Conversations in which this bot has + * participated. GET from this method with a skip token The return value is + * a ConversationsResult, which contains an array of ConversationMembers + * and a skip token. If the skip token is not empty, then there are further + * values to be returned. Call this method again with the returned token to + * get more values. Each ConversationMembers Object contains the D of the + * conversation and an array of ChannelAccounts that describe the members + * of the conversation. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param continuationToken skip or continuation token. + * + * @return task for ConversationsResult. + */ + protected CompletableFuture onGetConversations( + ClaimsIdentity claimsIdentity, + String conversationId, + String continuationToken) { + throw new NotImplementedException("onGetConversationMembers is not implemented"); + } + + /** + * GetConversationMembers() API for Skill. + * + * Override this method to enumerate the members of a conversation. This + * REST API takes a ConversationId and returns an array of ChannelAccount + * Objects representing the members of the conversation. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * + * @return task for a response. + */ + protected CompletableFuture> onGetConversationMembers( + ClaimsIdentity claimsIdentity, + String conversationId) { + throw new NotImplementedException("onGetConversationMembers is not implemented"); + } + + /** + * GetConversationPagedMembers() API for Skill. + * + * Override this method to enumerate the members of a conversation one page + * at a time. This REST API takes a ConversationId. Optionally a pageSize + * and/or continuationToken can be provided. It returns a + * PagedMembersResult, which contains an array of ChannelAccounts + * representing the members of the conversation and a continuation token + * that can be used to get more values. One page of ChannelAccounts records + * are returned with each call. The number of records in a page may vary + * between channels and calls. The pageSize parameter can be used as a + * suggestion. If there are no additional results the response will not + * contain a continuation token. If there are no members in the + * conversation the Members will be empty or not present in the response. A + * response to a request that has a continuation token from a prior request + * may rarely return members from a previous request. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param pageSize Suggested page size. + * @param continuationToken Continuation Token. + * + * @return task for a response. + */ + protected CompletableFuture onGetConversationPagedMembers( + ClaimsIdentity claimsIdentity, + String conversationId, + Integer pageSize, + String continuationToken) { + throw new NotImplementedException("onGetConversationPagedMembers is not implemented"); + } + + /** + * DeleteConversationMember() API for Skill. + * + * Override this method to deletes a member from a conversation. This REST + * API takes a ConversationId and a memberId (of type String) and removes + * that member from the conversation. If that member was the last member of + * the conversation, the conversation will also be deleted. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param memberId D of the member to delete from this + * conversation. + * + * @return task. + */ + protected CompletableFuture onDeleteConversationMember( + ClaimsIdentity claimsIdentity, + String conversationId, + String memberId) { + throw new NotImplementedException("onDeleteConversationMember is not implemented"); + } + + /** + * SendConversationHistory() API for Skill. + * + * Override this method to this method allows you to upload the historic + * activities to the conversation. Sender must ensure that the historic + * activities have unique ids and appropriate timestamps. The ids are used + * by the client to deal with duplicate activities and the timestamps are + * used by the client to render the activities in the right order. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param transcript Transcript of activities. + * + * @return task for a resource response. + */ + protected CompletableFuture onSendConversationHistory( + ClaimsIdentity claimsIdentity, + String conversationId, + Transcript transcript) { + throw new NotImplementedException("onSendConversationHistory is not implemented"); + } + + /** + * UploadAttachment() API. + * + * Override this method to store data in a compliant store when dealing + * with enterprises. The response is a ResourceResponse which contains an + * AttachmentId which is suitable for using with the attachments API. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param attachmentUpload Attachment data. + * + * @return task with result. + */ + protected CompletableFuture onUploadAttachment( + ClaimsIdentity claimsIdentity, + String conversationId, + AttachmentData attachmentUpload) { + throw new NotImplementedException("onUploadAttachment is not implemented"); + } + + /** + * Helper to authenticate the header. + * + * This code is very similar to the code in + * {@link JwtTokenValidation#authenticateRequest(Activity, String, + * CredentialProvider, ChannelProvider, AuthenticationConfiguration, + * HttpClient)} , we should move this code somewhere in that library when + * we refactor auth, for now we keep it private to avoid adding more public + * static functions that we will need to deprecate later. + */ + private CompletableFuture authenticate(String authHeader) { + if (StringUtils.isEmpty(authHeader)) { + return credentialProvider.isAuthenticationDisabled().thenCompose(isAuthDisabled -> { + if (!isAuthDisabled) { + return Async.completeExceptionally( + // No auth header. Auth is required. Request is not authorized. + new AuthenticationException("No auth header, Auth is required. Request is not authorized") + ); + } + + // In the scenario where auth is disabled, we still want to have the + // IsAuthenticated flag set in the ClaimsIdentity. + // To do this requires adding in an empty claim. + // Since ChannelServiceHandler calls are always a skill callback call, we set the skill claim too. + return CompletableFuture.completedFuture(SkillValidation.createAnonymousSkillClaim()); + }); + } + + // Validate the header and extract claims. + return JwtTokenValidation.validateAuthHeader( + authHeader, credentialProvider, getChannelProvider(), "unknown", null, authConfiguration); + } + /** + * Gets the channel provider that implements {@link ChannelProvider} . + * @return the ChannelProvider value as a getChannelProvider(). + */ + protected ChannelProvider getChannelProvider() { + return this.channelProvider; + } + +} + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ComponentRegistration.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ComponentRegistration.java new file mode 100644 index 000000000..c19a39fc7 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ComponentRegistration.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * ComponentRegistration is a signature class for discovering assets from components. + */ +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +public class ComponentRegistration { + + private static final ConcurrentHashMap, Object> COMPONENTS = + new ConcurrentHashMap, Object>(); + + /** + * Add a component which implements registration methods. + * + * @param componentRegistration The component to add to the registration. + */ + public static void add(ComponentRegistration componentRegistration) { + COMPONENTS.put(componentRegistration.getClass(), componentRegistration); + } + + /** + * Gets list of all ComponentRegistration objects registered. + * + * @return A array of ComponentRegistration objects. + */ + public static Iterable getComponents() { + return COMPONENTS.values(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ConnectorClientBuilder.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ConnectorClientBuilder.java new file mode 100644 index 000000000..b63e19699 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ConnectorClientBuilder.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; + +/** + * Abstraction to build connector clients. + */ + public interface ConnectorClientBuilder { + + /** + * Creates the connector client asynchronous. + * @param serviceUrl The service URL. + * @param claimsIdentity The claims claimsIdentity. + * @param audience The target audience for the connector. + * @return ConnectorClient instance. + */ + CompletableFuture createConnectorClient(String serviceUrl, + ClaimsIdentity claimsIdentity, + String audience); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ConversationState.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ConversationState.java new file mode 100644 index 000000000..0b22ff887 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ConversationState.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.apache.commons.lang3.StringUtils; + +/** + * Defines a state management object for conversation state. + */ +public class ConversationState extends BotState { + /** + * Creates a new {@link ConversationState} object. + * + * @param withStorage The storage layer to use. + */ + public ConversationState(Storage withStorage) { + super(withStorage, ConversationState.class.getSimpleName()); + } + + /** + * Gets the key to use when reading and writing state to and from storage. + * + * @param turnContext The context object for this turn. + * @return The storage key. + */ + @Override + public String getStorageKey(TurnContext turnContext) { + if (turnContext.getActivity() == null) { + throw new IllegalArgumentException("invalid activity"); + } + + if (StringUtils.isEmpty(turnContext.getActivity().getChannelId())) { + throw new IllegalArgumentException("invalid activity-missing channelId"); + } + + if ( + turnContext.getActivity().getConversation() == null + || StringUtils.isEmpty(turnContext.getActivity().getConversation().getId()) + ) { + throw new IllegalArgumentException("invalid activity-missing Conversation.Id"); + } + + // {channelId}/conversations/{conversationId} + return turnContext.getActivity().getChannelId() + "/conversations/" + + turnContext.getActivity().getConversation().getId(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java new file mode 100644 index 000000000..db0999daa --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DelegatingTurnContext.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * A TurnContext that wraps an untyped inner TurnContext. + */ +public class DelegatingTurnContext implements TurnContext { + /** + * The TurnContext being wrapped. + */ + private TurnContext innerTurnContext; + + /** + * Initializes a new instance of the DelegatingTurnContext class. + * + * @param withTurnContext The TurnContext to wrap. + */ + public DelegatingTurnContext(TurnContext withTurnContext) { + innerTurnContext = withTurnContext; + } + + /** + * Gets the locale on this context object. + * @return The string of locale on this context object. + */ + @Override + public String getLocale() { + return innerTurnContext.getLocale(); + } + + /** + * Set the locale on this context object. + * @param withLocale The string of locale on this context object. + */ + @Override + public void setLocale(String withLocale) { + innerTurnContext.setLocale(withLocale); + } + + /** + * Gets the inner context's activity. + * + * @return The inner {@link TurnContext#getAdapter()}. + */ + @Override + public BotAdapter getAdapter() { + return innerTurnContext.getAdapter(); + } + + /** + * Gets the inner context's activity. + * + * @return The inner {@link TurnContext#getTurnState()}. + */ + @Override + public TurnContextStateCollection getTurnState() { + return innerTurnContext.getTurnState(); + } + + /** + * Gets the inner context's activity. + * + * @return The inner {@link TurnContext#getActivity()}. + */ + @Override + public Activity getActivity() { + return innerTurnContext.getActivity(); + } + + /** + * Gets the inner context's responded value. + * + * @return The inner {@link TurnContext#getResponded()}. + */ + @Override + public boolean getResponded() { + return innerTurnContext.getResponded(); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture sendActivity(String textReplyToSend) { + return innerTurnContext.sendActivity(textReplyToSend); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture sendActivity(String textReplyToSend, String speak) { + return innerTurnContext.sendActivity(textReplyToSend, speak); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture sendActivity( + String textReplyToSend, + String speak, + InputHints inputHint + ) { + return innerTurnContext.sendActivity(textReplyToSend, speak, inputHint); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture sendActivity(Activity activity) { + return innerTurnContext.sendActivity(activity); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture sendActivities(List activities) { + return innerTurnContext.sendActivities(activities); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture updateActivity(Activity activity) { + return innerTurnContext.updateActivity(activity); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture deleteActivity(String activityId) { + return innerTurnContext.deleteActivity(activityId); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public CompletableFuture deleteActivity(ConversationReference conversationReference) { + return innerTurnContext.deleteActivity(conversationReference); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public TurnContext onSendActivities(SendActivitiesHandler handler) { + return innerTurnContext.onSendActivities(handler); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public TurnContext onUpdateActivity(UpdateActivityHandler handler) { + return innerTurnContext.onUpdateActivity(handler); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public TurnContext onDeleteActivity(DeleteActivityHandler handler) { + return innerTurnContext.onDeleteActivity(handler); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DeleteActivityHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DeleteActivityHandler.java new file mode 100644 index 000000000..1e69c91a9 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/DeleteActivityHandler.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.ConversationReference; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * A method that can participate in delete activity events for the current turn. + */ +@FunctionalInterface +public interface DeleteActivityHandler { + /** + * A method that can participate in delete activity events for the current turn. + * + * @param context The context object for the turn. + * @param reference The conversation containing the activity. + * @param next The delegate to call to continue event processing. + * @return A task that represents the work queued to execute. A handler calls + * the {@code next} delegate to pass control to the next registered + * handler. If a handler doesn’t call the next delegate, the adapter + * does not call any of the subsequent handlers and does not delete the + * activity. + *

+ * The conversation reference's + * {@link ConversationReference#getActivityId} indicates the activity in + * the conversation to replace. + *

+ */ + CompletableFuture invoke( + TurnContext context, + ConversationReference reference, + Supplier> next + ); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/EventFactory.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/EventFactory.java new file mode 100644 index 000000000..a7c7e06fc --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/EventFactory.java @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.UUID; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.Entity; +import com.microsoft.bot.schema.HandoffEventNames; +import com.microsoft.bot.schema.Transcript; + +import org.apache.commons.lang3.StringUtils; + +/** + * Contains utility methods for creating various event types. + */ +public final class EventFactory { + + private EventFactory() { + + } + + /** + * Create handoff initiation event. + * + * @param turnContext turn context. + * @param handoffContext agent hub-specific context. + * + * @return handoff event. + */ + public static Activity createHandoffInitiation(TurnContext turnContext, Object handoffContext) { + return createHandoffInitiation(turnContext, handoffContext, null); + } + + + /** + * Create handoff initiation event. + * + * @param turnContext turn context. + * @param handoffContext agent hub-specific context. + * @param transcript transcript of the conversation. + * + * @return handoff event. + */ + public static Activity createHandoffInitiation(TurnContext turnContext, Object handoffContext, + Transcript transcript) { + if (turnContext == null) { + throw new IllegalArgumentException("turnContext cannot be null."); + } + + Activity handoffEvent = createHandoffEvent(HandoffEventNames.INITIATEHANDOFF, handoffContext, + turnContext.getActivity().getConversation()); + + handoffEvent.setFrom(turnContext.getActivity().getFrom()); + handoffEvent.setRelatesTo(turnContext.getActivity().getConversationReference()); + handoffEvent.setReplyToId(turnContext.getActivity().getId()); + handoffEvent.setServiceUrl(turnContext.getActivity().getServiceUrl()); + handoffEvent.setChannelId(turnContext.getActivity().getChannelId()); + + if (transcript != null) { + Attachment attachment = new Attachment(); + attachment.setContent(transcript); + attachment.setContentType("application/json"); + attachment.setName("Transcript"); + handoffEvent.getAttachments().add(attachment); + } + + return handoffEvent; + } + + + /** + * Create handoff status event. + * + * @param conversation Conversation being handed over. + * @param state State, possible values are: "accepted", "failed", + * "completed". + * + * @return handoff event. + */ + public static Activity createHandoffStatus(ConversationAccount conversation, String state) { + return createHandoffStatus(conversation, state, null); + } + + /** + * Create handoff status event. + * + * @param conversation Conversation being handed over. + * @param state State, possible values are: "accepted", "failed", + * "completed". + * @param message Additional message for failed handoff. + * + * @return handoff event. + */ + public static Activity createHandoffStatus(ConversationAccount conversation, String state, String message) { + if (conversation == null) { + throw new IllegalArgumentException("conversation cannot be null."); + } + + if (state == null) { + throw new IllegalArgumentException("state cannot be null."); + } + + ObjectNode handoffContext = JsonNodeFactory.instance.objectNode(); + handoffContext.set("state", JsonNodeFactory.instance.textNode(state)); + if (StringUtils.isNotBlank(message)) { + handoffContext.set("message", JsonNodeFactory.instance.textNode(message)); + } + + Activity handoffEvent = createHandoffEvent(HandoffEventNames.HANDOFFSTATUS, handoffContext, conversation); + return handoffEvent; + } + + private static Activity createHandoffEvent(String name, Object value, ConversationAccount conversation) { + Activity handoffEvent = Activity.createEventActivity(); + + handoffEvent.setName(name); + handoffEvent.setValue(value); + handoffEvent.setId(UUID.randomUUID().toString()); + handoffEvent.setTimestamp(OffsetDateTime.now(ZoneId.of("UTC"))); + handoffEvent.setConversation(conversation); + handoffEvent.setAttachments(new ArrayList()); + handoffEvent.setEntities(new ArrayList()); + return handoffEvent; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/IntentScore.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/IntentScore.java new file mode 100644 index 000000000..db517b949 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/IntentScore.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.HashMap; +import java.util.Map; + +/** + * Score plus any extra information about an intent. + */ +public class IntentScore { + /** + * Confidence in an intent. + */ + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private double score; + + /** + * Extra properties to include in the results. + */ + private HashMap properties = new HashMap<>(); + + /** + * Gets confidence in an intent. + * + * @return Confidence in an intent. + */ + public double getScore() { + return score; + } + + /** + * Sets confidence in an intent. + * + * @param withScore Confidence in an intent. + */ + public void setScore(double withScore) { + score = withScore; + } + + /** + * Gets extra properties to include in the results. + * + * @return Any extra properties to include in the results. + */ + @JsonAnyGetter + public Map getProperties() { + return this.properties; + } + + /** + * Sets extra properties to include in the results. + * + * @param key The key of the property. + * @param value The JsonNode value of the property. + */ + @JsonAnySetter + public void setProperties(String key, JsonNode value) { + this.properties.put(key, value); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java new file mode 100644 index 000000000..483c23de4 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * Tuple class containing an HTTP Status Code and a JSON Serializable object. + * The HTTP Status code is, in the invoke activity scenario, what will be set in + * the resulting POST. The Body of the resulting POST will be the JSON + * Serialized content from the Body property. + */ +public class InvokeResponse { + + /** + * The POST that is generated in response to the incoming Invoke Activity will + * have the HTTP Status code specified by this field. + */ + private int status; + /** + * The POST that is generated in response to the incoming Invoke Activity will + * have a body generated by JSON serializing the object in the Body field. + */ + private Object body; + + /** + * Initializes new instance of InvokeResponse. + * + * @param withStatus The invoke response status. + * @param withBody The invoke response body. + */ + public InvokeResponse(int withStatus, Object withBody) { + status = withStatus; + body = withBody; + } + + /** + * Gets the HTTP status code for the response. + * + * @return The HTTP status code. + */ + public int getStatus() { + return status; + } + + /** + * Sets the HTTP status code for the response. + * + * @param withStatus The HTTP status code. + */ + public void setStatus(int withStatus) { + this.status = withStatus; + } + + /** + * Gets the body content for the response. + * + * @return The body content. + */ + public Object getBody() { + return body; + } + + /** + * Sets the body content for the response. + * + * @param withBody The body content. + */ + public void setBody(Object withBody) { + body = withBody; + } + + /** + * Returns if the status of the request was successful. + * @return True if the status code is successful, false if not. + */ + @SuppressWarnings("MagicNumber") + public boolean getIsSuccessStatusCode() { + return status >= 200 && status <= 299; + } + +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryStorage.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryStorage.java new file mode 100644 index 000000000..dcf74bafc --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryStorage.java @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.connector.Async; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A storage layer that uses an in-memory dictionary. + */ +public class MemoryStorage implements Storage { + /** + * Special field for holding the type information for the top level object being + * stored. + */ + private static final String TYPENAMEFORNONENTITY = "__type_name_"; + + /** + * Concurrency sync. + */ + private final Object syncroot = new Object(); + + /** + * To/From JSON. + */ + private ObjectMapper objectMapper; + + /** + * The internal map for storage. + */ + private Map memory; + + /** + * The... ummm... logger. + */ + private Logger logger = LoggerFactory.getLogger(MemoryStorage.class); + + /** + * eTag counter. + */ + private int eTag = 0; + + /** + * Initializes a new instance of the MemoryStorage class. + */ + public MemoryStorage() { + this(null); + } + + /** + * Initializes a new instance of the MemoryStorage class. + * + * @param values A pre-existing dictionary to use; or null to use a new one. + */ + public MemoryStorage(Map values) { + objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .findAndRegisterModules(); + objectMapper.enableDefaultTyping(); + + memory = values != null ? values : new ConcurrentHashMap<>(); + } + + /** + * Reads storage items from storage. + * + * @param keys keys of the items to read + * @return A task that represents the work queued to execute. If the activities + * are successfully sent, the task result contains the items read, + * indexed by key. + */ + @Override + public CompletableFuture> read(String[] keys) { + if (keys == null) { + return Async.completeExceptionally(new IllegalArgumentException("keys cannot be null")); + } + + Map storeItems = new ConcurrentHashMap<>(keys.length); + synchronized (this.syncroot) { + for (String key : keys) { + if (memory.containsKey(key)) { + JsonNode stateNode = memory.get(key); + if (stateNode != null) { + try { + // Check if type info is set for the class + if (!(stateNode.hasNonNull(TYPENAMEFORNONENTITY))) { + logger.error("Read failed: Type info not present for " + key); + return Async.completeExceptionally(new RuntimeException( + String + .format("Read failed: Type info not present for key " + key) + )); + } + String clsName = stateNode.get(TYPENAMEFORNONENTITY).textValue(); + + // Load the class info + Class cls; + try { + cls = Class.forName(clsName); + } catch (ClassNotFoundException e) { + logger.error("Read failed: Could not load class {}", clsName); + return Async.completeExceptionally(new RuntimeException( + String.format("Read failed: Could not load class %s", clsName) + )); + } + + // Populate dictionary + storeItems.put(key, objectMapper.treeToValue(stateNode, cls)); + } catch (JsonProcessingException e) { + logger.error("Read failed: {}", e.toString()); + return Async.completeExceptionally(new RuntimeException( + String.format("Read failed: %s", e.toString()) + )); + } + } + } + } + } + + return CompletableFuture.completedFuture(storeItems); + } + + /** + * Writes storage items to storage. + * + * @param changes The items to write, indexed by key. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture write(Map changes) { + synchronized (this.syncroot) { + for (Map.Entry change : changes.entrySet()) { + Object newValue = change.getValue(); + + String oldStateETag = null; + if (memory.containsKey(change.getKey())) { + JsonNode oldState = memory.get(change.getKey()); + if (oldState.has("eTag")) { + JsonNode eTagToken = oldState.get("eTag"); + oldStateETag = eTagToken.asText(); + } + } + + // Dictionary stores Key:JsonNode (with type information held within the + // JsonNode) + JsonNode newState = objectMapper.valueToTree(newValue); + ((ObjectNode) newState) + .put(TYPENAMEFORNONENTITY, newValue.getClass().getTypeName()); + + // Set ETag if applicable + if (newValue instanceof StoreItem) { + StoreItem newStoreItem = (StoreItem) newValue; + if ( + oldStateETag != null && !StringUtils.equals(newStoreItem.getETag(), "*") + && !StringUtils.equals(newStoreItem.getETag(), oldStateETag) + ) { + String msg = String.format( + "eTag conflict. Original: %s, Current: %s", newStoreItem.getETag(), + oldStateETag + ); + logger.error(msg); + return Async.completeExceptionally(new RuntimeException(msg)); + } + int newTag = eTag++; + ((ObjectNode) newState).put("eTag", Integer.toString(newTag)); + } + + memory.put(change.getKey(), newState); + } + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Deletes storage items from storage. + * + * @param keys keys of the items to delete + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture delete(String[] keys) { + if (keys == null) { + return Async.completeExceptionally(new IllegalArgumentException("keys cannot be null")); + } + + synchronized (this.syncroot) { + for (String key : keys) { + memory.remove(key); + } + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java new file mode 100644 index 000000000..3bdb35abf --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MemoryTranscriptStore.java @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.codepoetics.protonpack.StreamUtils; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The memory transcript store stores transcripts in volatile memory in a + * Dictionary. + * + *

+ * Because this uses an unbounded volatile dictionary this should only be used + * for unit tests or non-production environments. + *

+ */ +public class MemoryTranscriptStore implements TranscriptStore { + /** + * Numbers of results in a paged request. + */ + private static final int PAGE_SIZE = 20; + + /** + * Sync object for locking. + */ + private final Object sync = new Object(); + + /** + * Map of channel transcripts. + */ + private HashMap>> channels = new HashMap<>(); + + /** + * Logs an activity to the transcript. + * + * @param activity The activity to log. + * @return A CompletableFuture that represents the work queued to execute. + */ + @Override + public final CompletableFuture logActivity(Activity activity) { + if (activity == null) { + return Async.completeExceptionally( + new IllegalArgumentException("activity cannot be null for LogActivity()")); + } + + synchronized (sync) { + HashMap> channel; + if (!channels.containsKey(activity.getChannelId())) { + channel = new HashMap<>(); + channels.put(activity.getChannelId(), channel); + } else { + channel = channels.get(activity.getChannelId()); + } + + ArrayList transcript; + + if (!channel.containsKey(activity.getConversation().getId())) { + transcript = new ArrayList<>(); + channel.put(activity.getConversation().getId(), transcript); + } else { + transcript = channel.get(activity.getConversation().getId()); + } + + transcript.add(activity); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @param continuationToken The continuation token from the previous page of + * results. + * @param startDate A cutoff date. Activities older than this date are + * not included. + * @return A task that represents the work queued to execute. If the task + * completes successfully, the result contains the matching activities. + */ + @Override + public CompletableFuture> getTranscriptActivities( + String channelId, + String conversationId, + String continuationToken, + OffsetDateTime startDate + ) { + if (channelId == null) { + return Async.completeExceptionally( + new IllegalArgumentException(String.format("missing %1$s", "channelId"))); + } + + if (conversationId == null) { + return Async.completeExceptionally( + new IllegalArgumentException(String.format("missing %1$s", "conversationId"))); + } + + PagedResult pagedResult = new PagedResult<>(); + synchronized (sync) { + if (channels.containsKey(channelId)) { + HashMap> channel = channels.get(channelId); + if (channel.containsKey(conversationId)) { + OffsetDateTime effectiveStartDate = (startDate == null) + ? OffsetDateTime.MIN + : startDate; + + ArrayList transcript = channel.get(conversationId); + + Stream stream = transcript.stream() + .sorted(Comparator.comparing(Activity::getTimestamp)) + .filter(a -> a.getTimestamp().compareTo(effectiveStartDate) >= 0); + + if (continuationToken != null) { + stream = StreamUtils + .skipWhile(stream, a -> !a.getId().equals(continuationToken)) + .skip(1); + } + + List items = stream.limit(PAGE_SIZE).collect(Collectors.toList()); + + pagedResult.setItems(items); + if (pagedResult.getItems().size() == PAGE_SIZE) { + pagedResult.setContinuationToken(items.get(items.size() - 1).getId()); + } + } + } + } + + return CompletableFuture.completedFuture(pagedResult); + } + + /** + * Deletes conversation data from the store. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation to delete. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture deleteTranscript(String channelId, String conversationId) { + if (channelId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + String.format("%1$s should not be null", "channelId") + )); + } + + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + String.format("%1$s should not be null", "conversationId") + )); + } + + synchronized (sync) { + if (this.channels.containsKey(channelId)) { + HashMap> channel = this.channels.get(channelId); + channel.remove(conversationId); + } + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Gets the conversations on a channel from the store. + * + * @param channelId The ID of the channel. + * @param continuationToken The continuation token from the previous page of + * results. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture> listTranscripts( + String channelId, + String continuationToken + ) { + if (channelId == null) { + return Async.completeExceptionally(new IllegalArgumentException(String.format( + "missing %1$s", "channelId" + ))); + } + + PagedResult pagedResult = new PagedResult<>(); + synchronized (sync) { + if (channels.containsKey(channelId)) { + HashMap> channel = channels.get(channelId); + Stream stream = channel.entrySet().stream().map(c -> { + OffsetDateTime offsetDateTime; + + if (c.getValue().stream().findFirst().isPresent()) { + offsetDateTime = c.getValue().stream().findFirst().get().getTimestamp(); + } else { + offsetDateTime = OffsetDateTime.now(); + } + + return new TranscriptInfo(c.getKey(), channelId, offsetDateTime); + }).sorted(Comparator.comparing(TranscriptInfo::getCreated)); + + if (continuationToken != null) { + stream = StreamUtils + .skipWhile(stream, c -> !c.getId().equals(continuationToken)) + .skip(1); + } + + List items = stream.limit(PAGE_SIZE).collect(Collectors.toList()); + + pagedResult.setItems(items); + if (items.size() == PAGE_SIZE) { + pagedResult.setContinuationToken(items.get(items.size() - 1).getId()); + } + } + } + + return CompletableFuture.completedFuture(pagedResult); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MessageFactory.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MessageFactory.java new file mode 100644 index 000000000..fef50ffc0 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MessageFactory.java @@ -0,0 +1,390 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.AttachmentLayoutTypes; +import com.microsoft.bot.schema.CardAction; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.bot.schema.SuggestedActions; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Contains utility methods for various message types a bot can return. + *

+ * Create and send a message. + * Activity message = MessageFactory.text("Hello World"); + * conext.sendActivity(message); + * + * + *

+ * The following apply to message actions in general. See the channel's + * documentation for limits imposed upon the contents of the text of the message + * to send. + *

+ * + *

+ * To control various characteristics of your bot's speech such as voice, rate, + * volume, pronunciation, and pitch, specify test to speak in Speech Synthesis + * Markup Language (SSML) format. + *

+ * + *

+ * Channels decide how each card action manifests in their user experience. In + * most cases, the cards are clickable. In others, they may be selected by + * speech input. In cases where the channel does not offer an interactive + * activation experience (e.g., when interacting over SMS), the channel may not + * support activation whatsoever. The decision about how to render actions is + * controlled by normative requirements elsewhere in this document (e.g. within + * the card format, or within the suggested actions definition). + *

+ */ +public final class MessageFactory { + private MessageFactory() { + + } + + /** + * Returns a simple text message. + * + * @param text The text of the message to send. + * @return A message activity containing the text. + */ + public static Activity text(String text) { + return text(text, null, null); + } + + /** + * Returns a simple text message. + * + * @param text The text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enabled + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity containing the text. + */ + public static Activity text(String text, String ssml, InputHints inputHint) { + Activity activity = Activity.createMessageActivity(); + setTextAndSpeech(activity, text, ssml, inputHint); + return activity; + } + + /** + * Returns a message that includes a set of suggested actions and optional text. + * + * + * // Create the activity and add suggested actions. + * Activity activity = MessageFactory.suggestedActions( + * new String[] { "red", "green", "blue" }, + * "Choose a color"); + *

+ * // Send the activity as a reply to the user. + * context.sendActivity(activity); + * + * + * @param actions The text of the actions to create. + * @param text Optional. The text of the message to send. + * @return A message activity containing the suggested actions. + */ + public static Activity suggestedActions(List actions, String text) { + return suggestedActions(actions, text, null, null); + } + + /** + * Returns a message that includes a set of suggested actions and optional text. + * + * + * // Create the activity and add suggested actions. + * Activity activity = MessageFactory.suggestedActions( + * new String[] { "red", "green", "blue" }, + * "Choose a color"); + *

+ * // Send the activity as a reply to the user. + * context.sendActivity(activity); + * + * + * @param actions The text of the actions to create. + * @param text Optional. The text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enable + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity containing the suggested actions. + */ + public static Activity suggestedActions( + List actions, + String text, + String ssml, + InputHints inputHint + ) { + if (actions == null) { + throw new IllegalArgumentException("actions cannot be null"); + } + + List cardActions = new ArrayList<>(); + for (String action : actions) { + CardAction cardAction = new CardAction(); + cardAction.setType(ActionTypes.IM_BACK); + cardAction.setValue(action); + cardAction.setTitle(action); + cardActions.add(cardAction); + } + + return suggestedCardActions(cardActions, text, ssml, inputHint); + } + + /** + * Returns a message that includes a set of suggested actions and optional text. + * + * @param actions The card actions to include. + * @param text Optional, the text of the message to send. + * @return A message activity that contains the suggested actions. + */ + public static Activity suggestedCardActions(List actions, String text) { + return suggestedCardActions(actions, text, null, null); + } + + /** + * Returns a message that includes a set of suggested actions and optional text. + * + * @param actions The card actions to include. + * @param text Optional, the text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enable + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity that contains the suggested actions. + */ + public static Activity suggestedCardActions( + List actions, + String text, + String ssml, + InputHints inputHint + ) { + if (actions == null) { + throw new IllegalArgumentException("actions cannot be null"); + } + + Activity activity = Activity.createMessageActivity(); + setTextAndSpeech(activity, text, ssml, inputHint); + + activity.setSuggestedActions(new SuggestedActions(actions.toArray(new CardAction[0]))); + + return activity; + } + + /** + * Returns a message activity that contains an attachment. + * + * @param attachment Attachment to include in the message. + * @return A message activity containing the attachment. + */ + public static Activity attachment(Attachment attachment) { + return attachment(attachment, null, null, null); + } + + /** + * Returns a message activity that contains an attachment. + * + * @param attachments Attachments to include in the message. + * @return A message activity containing the attachment. + */ + public static Activity attachment(List attachments) { + return attachment(attachments, null, null, null); + } + + /** + * Returns a message activity that contains an attachment. + * + * @param attachment Attachment to include in the message. + * @param text Optional, the text of the message to send. + * @return A message activity containing the attachment. + */ + public static Activity attachment(Attachment attachment, String text) { + return attachment(attachment, text, null, null); + } + + /** + * Returns a message activity that contains an attachment. + * + * @param attachment Attachment to include in the message. + * @param text Optional, the text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enable + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity containing the attachment. + */ + public static Activity attachment( + Attachment attachment, + String text, + String ssml, + InputHints inputHint + ) { + if (attachment == null) { + throw new IllegalArgumentException("attachment cannot be null"); + } + + return attachment(Collections.singletonList(attachment), text, ssml, inputHint); + } + + /** + * Returns a message activity that contains an attachment. + * + * @param attachments Attachments to include in the message. + * @param text Optional, the text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enable + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity containing the attachment. + */ + public static Activity attachment( + List attachments, + String text, + String ssml, + InputHints inputHint + ) { + if (attachments == null) { + throw new IllegalArgumentException("attachments cannot be null"); + } + + return attachmentActivity(AttachmentLayoutTypes.LIST, attachments, text, ssml, inputHint); + } + + /** + * Returns a message activity that contains a collection of attachments, in a + * list. + * + * @param attachments Attachments to include in the message. + * @param text Optional, the text of the message to send. + * @return A message activity containing the attachment. + */ + public static Activity carousel(List attachments, String text) { + return carousel(attachments, text, null, null); + } + + /** + * Returns a message activity that contains a collection of attachments, in a + * list. + * + * @param attachments Attachments to include in the message. + * @param text Optional, the text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enable + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity containing the attachment. + */ + public static Activity carousel( + List attachments, + String text, + String ssml, + InputHints inputHint + ) { + if (attachments == null) { + throw new IllegalArgumentException("attachments cannot be null"); + } + + return attachmentActivity( + AttachmentLayoutTypes.CAROUSEL, attachments, text, ssml, inputHint + ); + } + + /** + * Returns a message activity that contains a single image or video. + * + * @param url The URL of the image or video to send. + * @param contentType The MIME type of the image or video. + * @return A message activity containing the attachment. + */ + public static Activity contentUrl(String url, String contentType) { + return contentUrl(url, contentType, null, null, null, null); + } + + /** + * Returns a message activity that contains a single image or video. + * + * @param url The URL of the image or video to send. + * @param contentType The MIME type of the image or video. + * @param name Optional, the name of the image or video file. + * @param text Optional, the text of the message to send. + * @param ssml Optional, text to be spoken by your bot on a speech-enable + * channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. Default is + * {@link InputHints#ACCEPTING_INPUT}. + * @return A message activity containing the attachment. + */ + public static Activity contentUrl( + String url, + String contentType, + String name, + String text, + String ssml, + InputHints inputHint + ) { + if (StringUtils.isEmpty(url)) { + throw new IllegalArgumentException("url cannot be null or empty"); + } + + if (StringUtils.isEmpty(contentType)) { + throw new IllegalArgumentException("contentType cannot be null or empty"); + } + + Attachment attachment = new Attachment(); + attachment.setContentType(contentType); + attachment.setContentUrl(url); + attachment.setName(StringUtils.isEmpty(name) ? null : name); + + return attachmentActivity( + AttachmentLayoutTypes.LIST, Collections.singletonList(attachment), text, ssml, inputHint + ); + } + + private static Activity attachmentActivity( + AttachmentLayoutTypes attachmentLayout, + List attachments, + String text, + String ssml, + InputHints inputHint + ) { + Activity activity = Activity.createMessageActivity(); + activity.setAttachmentLayout(attachmentLayout); + activity.setAttachments(attachments); + setTextAndSpeech(activity, text, ssml, inputHint); + return activity; + } + + private static void setTextAndSpeech( + Activity activity, + String text, + String ssml, + InputHints inputHint + ) { + activity.setText(StringUtils.isEmpty(text) ? null : text); + activity.setSpeak(StringUtils.isEmpty(ssml) ? null : ssml); + activity.setInputHint(inputHint == null ? InputHints.ACCEPTING_INPUT : inputHint); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Middleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Middleware.java new file mode 100644 index 000000000..34ddd9708 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Middleware.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * Represents middleware that can operate on incoming activities. A + * {@link BotAdapter} passes incoming activities from the user's channel to the + * middleware's {@link #onTurn(TurnContext, NextDelegate)} method. + *

+ * You can add middleware objects to your adapter’s middleware collection. The + * adapter processes and directs incoming activities in through the bot + * middleware pipeline to your bot’s logic and then back out again. As each + * activity flows in and out of the bot, each piece of middleware can inspect or + * act upon the activity, both before and after the bot logic runs. + *

+ *

+ * For each activity, the adapter calls middleware in the order in which you + * added it. + *

+ * + * This defines middleware that sends "before" and "after" messages before and + * after the adapter calls the bot's {@link Bot#onTurn(TurnContext)} method. + * + * {@code + * public class SampleMiddleware : Middleware + * { + * public async Task OnTurn(TurnContext context, MiddlewareSet.NextDelegate next) + * { + * context.SendActivity("before"); await next().ConfigureAwait(false); + * context.SendActivity("after"); } } } + * + * {@link Bot} + */ +public interface Middleware { + /** + * Processes an incoming activity. + * + * @param turnContext The context object for this turn. + * @param next The delegate to call to continue the bot middleware + * pipeline. + * @return A task that represents the work queued to execute. Middleware calls + * the {@code next} delegate to pass control to the next middleware in + * the pipeline. If middleware doesn’t call the next delegate, the + * adapter does not call any of the subsequent middleware’s request + * handlers or the bot’s receive handler, and the pipeline short + * circuits. + *

+ * The {@code context} provides information about the incoming activity, + * and other data needed to process the activity. + *

+ *

+ * {@link TurnContext} {@link com.microsoft.bot.schema.Activity} + */ + CompletableFuture onTurn(TurnContext turnContext, NextDelegate next); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MiddlewareSet.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MiddlewareSet.java new file mode 100644 index 000000000..77f910ba7 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MiddlewareSet.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Contains an ordered set of {@link Middleware}. + */ +public class MiddlewareSet implements Middleware { + /** + * List of {@link Middleware} objects this class manages. + */ + private final List middlewareList = new ArrayList<>(); + + /** + * Adds a middleware object to the end of the set. + * + * @param middleware The middleware to add. + * @return The updated middleware set. + */ + public MiddlewareSet use(Middleware middleware) { + BotAssert.middlewareNotNull(middleware); + this.middlewareList.add(middleware); + return this; + } + + /** + * Processes an incoming activity. + * + * @param turnContext The context object for this turn. + * @param next The delegate to call to continue the bot middleware + * pipeline. + * @return A task that represents the work queued to execute. Middleware calls + * the {@code next} delegate to pass control to the next middleware in + * the pipeline. If middleware doesn’t call the next delegate, the + * adapter does not call any of the subsequent middleware’s request + * handlers or the bot’s receive handler, and the pipeline short + * circuits. + *

+ * The {@code context} provides information about the incoming activity, + * and other data needed to process the activity. + *

+ *

+ * {@link TurnContext} {@link com.microsoft.bot.schema.Activity} + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + return receiveActivityInternal(turnContext, null).thenCompose((result) -> next.next()); + } + + /** + * Processes an activity. + * + * @param context The context object for the turn. + * @param callback The delegate to call when the set finishes processing the + * activity. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture receiveActivityWithStatus( + TurnContext context, + BotCallbackHandler callback + ) { + return receiveActivityInternal(context, callback); + } + + private CompletableFuture receiveActivityInternal( + TurnContext context, + BotCallbackHandler callback + ) { + return receiveActivityInternal(context, callback, 0); + } + + private CompletableFuture receiveActivityInternal( + TurnContext context, + BotCallbackHandler callback, + int nextMiddlewareIndex + ) { + // Check if we're at the end of the middleware list yet + if (nextMiddlewareIndex == middlewareList.size()) { + // If all the Middleware ran, the "leading edge" of the tree is now complete. + // This means it's time to run any developer specified callback. + // Once this callback is done, the "trailing edge" calls are then completed. + // This + // allows code that looks like: + // Trace.TraceInformation("before"); + // await next(); + // Trace.TraceInformation("after"); + // to run as expected. + + // If a callback was provided invoke it now and return its task, otherwise just + // return the completed task + if (callback == null) { + return CompletableFuture.completedFuture(null); + } else { + return callback.invoke(context); + } + } + + // Get the next piece of middleware + Middleware nextMiddleware = middlewareList.get(nextMiddlewareIndex); + + // Execute the next middleware passing a closure that will recurse back into + // this method at the + // next piece of middleware as the NextDelegate + return nextMiddleware.onTurn( + context, () -> receiveActivityInternal(context, callback, nextMiddlewareIndex + 1) + ); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/NextDelegate.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/NextDelegate.java new file mode 100644 index 000000000..ec89525a6 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/NextDelegate.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for the Middleware pipeline. + */ +@FunctionalInterface +public interface NextDelegate { + /** + * The delegate to call to continue the bot middleware pipeline. + * + * @return Future task. + */ + CompletableFuture next(); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/NullBotTelemetryClient.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/NullBotTelemetryClient.java new file mode 100644 index 000000000..e45217f1b --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/NullBotTelemetryClient.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Map; + +/** + * A no-op telemetry client. + */ +public class NullBotTelemetryClient implements BotTelemetryClient { + @SuppressWarnings("checkstyle:ParameterNumber") + @Override + public void trackAvailability( + String name, + OffsetDateTime timeStamp, + Duration duration, + String runLocation, + boolean success, + String message, + Map properties, + Map metrics + ) { + + } + + @SuppressWarnings("checkstyle:ParameterNumber") + @Override + public void trackDependency( + String dependencyTypeName, + String target, + String dependencyName, + String data, + OffsetDateTime startTime, + Duration duration, + String resultCode, + boolean success + ) { + + } + + @Override + public void trackEvent( + String eventName, + Map properties, + Map metrics + ) { + + } + + @Override + public void trackException( + Exception exception, + Map properties, + Map metrics + ) { + + } + + @Override + public void trackTrace(String message, Severity severityLevel, Map properties) { + + } + + @Override + public void flush() { + + } + + @Override + public void trackDialogView(String dialogName, Map properties, Map metrics) { + + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/OnTurnErrorHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/OnTurnErrorHandler.java new file mode 100644 index 000000000..3f699e89f --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/OnTurnErrorHandler.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * Error handler that can catch exceptions in the middleware or application. + */ +@FunctionalInterface +public interface OnTurnErrorHandler { + /** + * Error handler that can catch exceptions in the middleware or application. + * + * @param turnContext The context object for this turn. + * @param exception The exception thrown. + * @return A task that represents the work queued to execute. + */ + CompletableFuture invoke(TurnContext turnContext, Throwable exception); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PagedResult.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PagedResult.java new file mode 100644 index 000000000..8a48d0541 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PagedResult.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Page of results from an enumeration. + * + * @param The type of items in the results. + */ +public class PagedResult { + /** + * Page of items. + */ + private List items = new ArrayList<>(); + + /** + * Token used to page through multiple pages. + */ + private String continuationToken; + + /** + * Gets the page of items. + * + * @return The List of items. + */ + public List getItems() { + return this.items; + } + + /** + * Sets the page of items. + * + * @param value The List of items. + */ + public void setItems(List value) { + this.items = value; + } + + /** + * Gets the token for retrieving the next page of results. + * + * @return The Continuation Token to pass to get the next page of results. + */ + public String getContinuationToken() { + return continuationToken; + } + + /** + * Sets the token for retrieving the next page of results. + * + * @param withValue The Continuation Token to pass to get the next page of + * results. + */ + public void setContinuationToken(String withValue) { + continuationToken = withValue; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PrivateConversationState.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PrivateConversationState.java new file mode 100644 index 000000000..0dc2570c8 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PrivateConversationState.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.apache.commons.lang3.StringUtils; + +/** + * Handles persistence of a conversation state object using the conversation.Id + * and from.Id part of an activity. + */ +public class PrivateConversationState extends BotState { + /** + * Initializes a new instance of the PrivateConversationState class. + * + * @param storage The storage provider to use. + */ + public PrivateConversationState(Storage storage) { + super(storage, PrivateConversationState.class.getSimpleName()); + } + + /** + * Gets the key to use when reading and writing state to and from storage. + * + * @param turnContext The context object for this turn. + * @return The storage key. + */ + @Override + public String getStorageKey(TurnContext turnContext) throws IllegalArgumentException { + if (turnContext.getActivity() == null) { + throw new IllegalArgumentException("invalid activity"); + } + + if (StringUtils.isEmpty(turnContext.getActivity().getChannelId())) { + throw new IllegalArgumentException("invalid activity-missing channelId"); + } + + if ( + turnContext.getActivity().getConversation() == null + || StringUtils.isEmpty(turnContext.getActivity().getConversation().getId()) + ) { + throw new IllegalArgumentException("invalid activity-missing Conversation.Id"); + } + + if ( + turnContext.getActivity().getFrom() == null + || StringUtils.isEmpty(turnContext.getActivity().getFrom().getId()) + ) { + throw new IllegalArgumentException("invalid activity-missing From.Id"); + } + + // {channelId}/conversations/{conversationId}/users/{userId} + return turnContext.getActivity().getChannelId() + "/conversations/" + + turnContext.getActivity().getConversation().getId() + "/users/" + + turnContext.getActivity().getFrom().getId(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PropertyManager.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PropertyManager.java new file mode 100644 index 000000000..e02a173d9 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/PropertyManager.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * PropertyManager defines implementation of a source of named properties. + */ +public interface PropertyManager { + /** + * Creates a managed state property accessor for a property. + * + * @param name The name of the property accessor. + * @param The property value type. + * @return A state property accessor for the property. + */ + StatePropertyAccessor createProperty(String name); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/QueueStorage.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/QueueStorage.java new file mode 100644 index 000000000..0665e8629 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/QueueStorage.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; + +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +/** + * A base class for enqueueing an Activity for later processing. + */ +public abstract class QueueStorage { + + /** + * Enqueues an Activity for later processing. The visibility timeout specifies how long the message + * should be invisible to Dequeue and Peek operations. + * @param activity The {@link Activity} to be queued for later processing. + * @param visibilityTimeout Visibility timeout. Optional with a default value of 0. Cannot be larger than 7 days. + * @param timeToLive Specifies the time-to-live interval for the message. + * @return A result string. + */ + public abstract CompletableFuture queueActivity(Activity activity, + @Nullable Duration visibilityTimeout, + @Nullable Duration timeToLive); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Recognizer.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Recognizer.java new file mode 100644 index 000000000..5a80b88c2 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Recognizer.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * Interface for Recognizers. + */ +public interface Recognizer { + /** + * Runs an utterance through a recognizer and returns a generic recognizer + * result. + * + * @param turnContext Turn context. + * @return Analysis of utterance. + */ + CompletableFuture recognize(TurnContext turnContext); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RecognizerConvert.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RecognizerConvert.java new file mode 100644 index 000000000..59eba866e --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RecognizerConvert.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * Can convert from a generic recognizer result to a strongly typed one. + */ +public interface RecognizerConvert { + /** + * Convert recognizer result. + * + * @param result Result to convert. + */ + void convert(Object result); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RecognizerResult.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RecognizerResult.java new file mode 100644 index 000000000..dbc459f98 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RecognizerResult.java @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.HashMap; +import java.util.Map; + +/** + * Contains recognition results generated by an {@link Recognizer}. + */ +public class RecognizerResult implements RecognizerConvert { + @JsonProperty(value = "entities") + private JsonNode entities; + + @JsonProperty(value = "text") + private String text; + + @JsonProperty(value = "alteredText") + private String alteredText; + + @JsonProperty(value = "intents") + private Map intents; + + /** + * Additional properties. + */ + private HashMap properties = new HashMap<>(); + + /** + * Holds intent score info. + */ + @SuppressWarnings({ "checkstyle:VisibilityModifier" }) + public static class NamedIntentScore { + /// The intent name + public String intent; + + /// The intent score + public double score; + } + + /** + * Return the top scoring intent and its score. + * + * @return The top scoring intent and score. + * @throws IllegalArgumentException No intents available. + */ + @JsonIgnore + public NamedIntentScore getTopScoringIntent() throws IllegalArgumentException { + if (getIntents() == null) { + throw new IllegalArgumentException("RecognizerResult.Intents cannot be null"); + } + + NamedIntentScore topIntent = new NamedIntentScore(); + for (Map.Entry intent : getIntents().entrySet()) { + double score = intent.getValue().getScore(); + if (score > topIntent.score) { + topIntent.intent = intent.getKey(); + topIntent.score = intent.getValue().getScore(); + } + } + + return topIntent; + } + + /** + * Gets the input text to recognize. + * + * @return The original text. + */ + public String getText() { + return text; + } + + /** + * Sets the input text to recognize. + * + * @param withText The text to recognize. + */ + public void setText(String withText) { + text = withText; + } + + /** + * Gets the input text as modified by the recognizer, for example for spelling + * correction. + * + * @return Text modified by recognizer. + */ + public String getAlteredText() { + return alteredText; + } + + /** + * Sets the input text as modified by the recognizer, for example for spelling + * correction. + * + * @param withAlteredText Text modified by recognizer. + */ + public void setAlteredText(String withAlteredText) { + alteredText = withAlteredText; + } + + /** + * Gets the recognized intents, with the intent as key and the confidence as + * value. + * + * @return Mapping from intent to information about the intent. + */ + public Map getIntents() { + return intents; + } + + /** + * Sets the recognized intents, with the intent as key and the confidence as + * value. + * + * @param withIntents Mapping from intent to information about the intent. + */ + public void setIntents(Map withIntents) { + intents = withIntents; + } + + /** + * Gets the recognized top-level entities. + * + * @return Object with each top-level recognized entity as a key. + */ + public JsonNode getEntities() { + return entities; + } + + /** + * Sets the recognized top-level entities. + * + * @param withEntities Object with each top-level recognized entity as a key. + */ + public void setEntities(JsonNode withEntities) { + entities = withEntities; + } + + /** + * Gets properties that are not otherwise defined by the RecognizerResult type + * but that might appear in the REST JSON object. + * + * @return The extended properties for the object. + */ + @JsonAnyGetter + public Map getProperties() { + return this.properties; + } + + /** + * Sets properties that are not otherwise defined by the RecognizerResult type + * but that might appear in the REST JSON object. + * + *

+ * With this, properties not represented in the defined type are not dropped + * when the JSON object is deserialized, but are instead stored in this + * property. Such properties will be written to a JSON object when the instance + * is serialized. + *

+ * + * @param key The property key. + * @param value The property value. + */ + @JsonAnySetter + public void setProperties(String key, JsonNode value) { + this.properties.put(key, value); + } + + /** + * Convert recognizer result. + * + * @param result Result to convert. + */ + @Override + public void convert(Object result) { + setText(((RecognizerResult) result).getText()); + setAlteredText(((RecognizerResult) result).getAlteredText()); + setIntents(((RecognizerResult) result).getIntents()); + setEntities(((RecognizerResult) result).getEntities()); + + for (String key : ((RecognizerResult) result).getProperties().keySet()) { + setProperties(key, ((RecognizerResult) result).getProperties().get(key)); + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RegisterClassMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RegisterClassMiddleware.java new file mode 100644 index 000000000..d74fe0f0d --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/RegisterClassMiddleware.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +import com.nimbusds.oauth2.sdk.util.StringUtils; + +/** + * Middleware for adding an object to or registering a service with the current + * turn context. + * + * @param The typeof service to add. + */ +public class RegisterClassMiddleware implements Middleware { + private String key; + + /** + * Initializes a new instance of the RegisterClassMiddleware class. + * + * @param service The Service to register. + */ + public RegisterClassMiddleware(T service) { + this.service = service; + } + + /** + * Initializes a new instance of the RegisterClassMiddleware class. + * + * @param service The Service to register. + * @param key optional key for service object in turn state. Default is name + * of service. + */ + public RegisterClassMiddleware(T service, String key) { + this.service = service; + this.key = key; + } + + private T service; + + /** + * Gets the Service. + * + * @return The Service. + */ + public T getService() { + return service; + } + + /** + * Sets the Service. + * + * @param withService The value to set the Service to. + */ + public void setService(T withService) { + this.service = withService; + } + + /** + * Adds the associated object or service to the current turn context. + * @param turnContext The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline. + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + if (!StringUtils.isBlank(key)) { + turnContext.getTurnState().add(key, service); + } else { + turnContext.getTurnState().add(service); + } + return next.next(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SendActivitiesHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SendActivitiesHandler.java new file mode 100644 index 000000000..b8b3e9d8e --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SendActivitiesHandler.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * A method that can participate in send activity events for the current turn. + */ +@FunctionalInterface +public interface SendActivitiesHandler { + /** + * A method that can participate in send activity events for the current turn. + * + * @param context The context object for the turn. + * @param activities The activities to send. + * @param next The delegate to call to continue event processing. + * @return A task that represents the work queued to execute. + */ + CompletableFuture invoke( + TurnContext context, + List activities, + Supplier> next + ); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SetSpeakMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SetSpeakMiddleware.java new file mode 100644 index 000000000..8fe2d1894 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SetSpeakMiddleware.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. +package com.microsoft.bot.builder; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; + +import org.apache.commons.lang3.StringUtils; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Support the DirectLine speech and telephony channels to ensure the + * appropriate SSML tags are set on the Activity Speak property. + */ +public class SetSpeakMiddleware implements Middleware { + + private final String voiceName; + private final boolean fallbackToTextForSpeak; + + /** + * Initializes a new instance of the {@link SetSpeakMiddleware} class. + * + * @param voiceName The SSML voice name attribute value. + * @param fallbackToTextForSpeak true if an empt Activity.Speak is populated + * with Activity.getText(). + */ + public SetSpeakMiddleware(String voiceName, boolean fallbackToTextForSpeak) { + this.voiceName = voiceName; + this.fallbackToTextForSpeak = fallbackToTextForSpeak; + } + + /** + * Processes an incoming activity. + * + * @param turnContext The context Object for this turn. + * @param next The delegate to call to continue the bot middleware + * pipeline. + * + * @return A task that represents the work queued to execute. + */ + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + turnContext.onSendActivities((ctx, activities, nextSend) -> { + for (Activity activity : activities) { + if (activity.getType().equals(ActivityTypes.MESSAGE)) { + if (fallbackToTextForSpeak && StringUtils.isBlank(activity.getSpeak())) { + activity.setSpeak(activity.getText()); + } + + if (StringUtils.isNotBlank(activity.getSpeak()) && StringUtils.isNotBlank(voiceName) + && (StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(), + Channels.DIRECTLINESPEECH) == 0 + || StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(), + Channels.EMULATOR) == 0 + || StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(), + "telephony") == 0)) { + if (!hasTag("speak", activity.getSpeak()) && !hasTag("voice", activity.getSpeak())) { + activity.setSpeak( + String.format("%s", voiceName, activity.getSpeak())); + } + activity.setSpeak(String + .format("%s", + activity.getLocale() != null ? activity.getLocale() : "en-US", + activity.getSpeak())); + } + } + } + return nextSend.get(); + }); + return next.next(); + } + + private boolean hasTag(String tagName, String speakText) { + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(speakText); + + if (doc.getElementsByTagName(tagName).getLength() > 0) { + return true; + } + + return false; + } catch (SAXException | ParserConfigurationException | IOException ex) { + return false; + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Severity.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Severity.java new file mode 100644 index 000000000..31c42b615 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Severity.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * This enumeration is used by TrackTrace to identify severity level. + */ +public enum Severity { + /** + * Verbose severity level. + */ + VERBOSE(0), + + /** + * Information severity level. + */ + INFORMATION(1), + + /** + * Warning severity level. + */ + WARNING(2), + + /** + * Error severity level. + */ + ERROR(3), + + /** + * Critical severity level. + */ + CRITICAL(4); + + private int value; + + /** + * Constructs with an in value. + * + * @param witValue Severity level. + */ + Severity(int witValue) { + value = witValue; + } + + /** + * For conversion to int. + * + * @return The int value of this enum. + */ + public int getSeverity() { + return value; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java new file mode 100644 index 000000000..e123282ab --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.SkillValidation; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** + * When added, this middleware will send typing activities back to the user when + * a Message activity is received to let them know that the bot has received the + * message and is working on the response. You can specify a delay in + * milliseconds before the first typing activity is sent and then a frequency, + * also in milliseconds which determines how often another typing activity is + * sent. Typing activities will continue to be sent until your bot sends another + * message back to the user. + */ +public class ShowTypingMiddleware implements Middleware { + private static final int DEFAULT_DELAY = 500; + private static final int DEFAULT_PERIOD = 2000; + + /** + * Initial delay before sending first typing indicator. Defaults to 500ms. + */ + private long delay; + + /** + * Rate at which additional typing indicators will be sent. Defaults to every + * 2000ms. + */ + private long period; + + /** + * Constructs with default delay and period. + */ + public ShowTypingMiddleware() { + this(DEFAULT_DELAY, DEFAULT_PERIOD); + } + + /** + * Initializes a new instance of the ShowTypingMiddleware class. + * + * @param withDelay Initial delay before sending first typing indicator. + * @param withPeriod Rate at which additional typing indicators will be sent. + * @throws IllegalArgumentException delay and period must be greater than zero + */ + public ShowTypingMiddleware(long withDelay, long withPeriod) throws IllegalArgumentException { + if (withDelay < 0) { + throw new IllegalArgumentException("Delay must be greater than or equal to zero"); + } + + if (withPeriod < 0) { + throw new IllegalArgumentException("Repeat period must be greater than zero"); + } + + delay = withDelay; + period = withPeriod; + } + + /** + * Processes an incoming activity. + * + * @param turnContext The context object for this turn. + * @param next The delegate to call to continue the bot middleware + * pipeline. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + if (!turnContext.getActivity().isType(ActivityTypes.MESSAGE) || isSkillBot(turnContext)) { + return next.next(); + } + + // do not await task - we want this to run in the background and we will cancel + // it when its done + CompletableFuture sendFuture = sendTyping(turnContext, delay, period); + return next.next().thenAccept(result -> sendFuture.cancel(true)); + } + + private static Boolean isSkillBot(TurnContext turnContext) { + Object identity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); + if (identity instanceof ClaimsIdentity) { + ClaimsIdentity claimsIdentity = (ClaimsIdentity) identity; + return SkillValidation.isSkillClaim(claimsIdentity.claims()); + } else { + return false; + } + } + + private static CompletableFuture sendTyping( + TurnContext turnContext, + long delay, + long period + ) { + return CompletableFuture.runAsync(() -> { + try { + Thread.sleep(delay); + + while (!Thread.currentThread().isInterrupted()) { + sendTypingActivity(turnContext).join(); + Thread.sleep(period); + } + } catch (InterruptedException e) { + // do nothing + } + }, ExecutorFactory.getExecutor()); + } + + private static CompletableFuture sendTypingActivity( + TurnContext turnContext + ) { + // create a TypingActivity, associate it with the conversation and send + // immediately + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + + // sending the Activity directly on the Adapter avoids other Middleware and + // avoids setting the Responded + // flag, however, this also requires that the conversation reference details are + // explicitly added. + ConversationReference conversationReference = turnContext.getActivity() + .getConversationReference(); + typingActivity.applyConversationReference(conversationReference); + + // make sure to send the Activity directly on the Adapter rather than via the + // TurnContext + return turnContext.getAdapter() + .sendActivities(turnContext, Collections.singletonList(typingActivity)); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SkypeMentionNormalizeMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SkypeMentionNormalizeMiddleware.java new file mode 100644 index 000000000..17305c555 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/SkypeMentionNormalizeMiddleware.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Entity; +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.CompletableFuture; + +/** + * Middleware to patch mention Entities from Skype since they don't conform to + * expected values. Bots that interact with Skype should use this middleware if + * mentions are used. + *

+ * A Skype mention "text" field is of the format: <at + * id=\"28:2bc5b54d-5d48-4ff1-bd25-03dcbb5ce918\">botname</at> But + * Activity.Text doesn't contain those tags and RemoveMentionText can't remove + * the entity from Activity.Text. This will remove the <at> nodes, leaving + * just the name. + */ +public class SkypeMentionNormalizeMiddleware implements Middleware { + /** + * Fixes incorrect Skype mention text. This will change the text value for all + * Skype mention entities. + * + * @param activity The Activity to correct. + */ + public static void normalizeSkypeMentionText(Activity activity) { + if ( + StringUtils.equals(activity.getChannelId(), Channels.SKYPE) + && StringUtils.equals(activity.getType(), ActivityTypes.MESSAGE) + ) { + + for (Entity entity : activity.getEntities()) { + if (StringUtils.equals(entity.getType(), "mention")) { + String text = entity.getProperties().get("text").asText(); + int closingBracket = text.indexOf(">"); + if (closingBracket != -1) { + int openingBracket = text.indexOf("<", closingBracket); + if (openingBracket != -1) { + String mention = text.substring(closingBracket + 1, openingBracket) + .trim(); + + // create new JsonNode with new mention value + JsonNode node = JsonNodeFactory.instance.textNode(mention); + entity.setProperties("text", node); + } + } + } + } + } + } + + /** + * Middleware implementation which corrects Entity.Mention.Text to a value + * RemoveMentionText can work with. + * + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline. + * @return + */ + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + normalizeSkypeMentionText(context.getActivity()); + return next.next(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StatePropertyAccessor.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StatePropertyAccessor.java new file mode 100644 index 000000000..1f6042a55 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StatePropertyAccessor.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * Interface which defines methods for how you can get data from a property + * source such as BotState. + * + * @param type of the property. + */ +public interface StatePropertyAccessor extends StatePropertyInfo { + /** + * Get the property value from the source. + * + * @param turnContext TurnContext. + * @return A task representing the result of the asynchronous operation. + */ + default CompletableFuture get(TurnContext turnContext) { + return get(turnContext, null); + } + + /** + * Get the property value from the source. + * + * @param turnContext TurnContext. + * @param defaultValueFactory Function which defines the property value to be + * returned if no value has been set. + * @return A task representing the result of the asynchronous operation. + */ + CompletableFuture get(TurnContext turnContext, Supplier defaultValueFactory); + + /** + * Delete the property from the source. + * + * @param turnContext TurnContext. + * @return A task representing the result of the asynchronous operation. + */ + CompletableFuture delete(TurnContext turnContext); + + /** + * Set the property value on the source. + * + * @param turnContext TurnContext. + * @param value The value to set. + * @return A task representing the result of the asynchronous operation. + */ + CompletableFuture set(TurnContext turnContext, T value); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StatePropertyInfo.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StatePropertyInfo.java new file mode 100644 index 000000000..c4a01d951 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StatePropertyInfo.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * This is metadata about the property including policy info. + */ +public interface StatePropertyInfo { + /** + * Gets the name of the property. + * + * @return The name of the property. + */ + String getName(); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Storage.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Storage.java new file mode 100644 index 000000000..c5706b9a0 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/Storage.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Defines the interface for a storage layer. + */ +public interface Storage { + /** + * Reads storage items from storage. + * + * @param keys keys of the items to read + * @return A task that represents the work queued to execute. If the activities + * are successfully sent, the task result contains the items read, + * indexed by key. + */ + CompletableFuture> read(String[] keys); + + /** + * Writes storage items to storage. + * + * @param changes The items to write, indexed by key. + * @return A task that represents the work queued to execute. + */ + CompletableFuture write(Map changes); + + /** + * Deletes storage items from storage. + * + * @param keys keys of the items to delete + * @return A task that represents the work queued to execute. + */ + CompletableFuture delete(String[] keys); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StoreItem.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StoreItem.java new file mode 100644 index 000000000..ab13c393e --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/StoreItem.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Exposes an ETag for concurrency control. + */ +public interface StoreItem { + /** + * Get eTag for concurrency. + * + * @return The eTag value. + */ + @JsonProperty(value = "eTag") + String getETag(); + + /** + * Set eTag for concurrency. + * + * @param withETag The eTag value. + */ + @JsonProperty(value = "eTag") + void setETag(String withETag); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryConstants.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryConstants.java new file mode 100644 index 000000000..5e23f07f8 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryConstants.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * Telemetry logger property names. + */ +public final class TelemetryConstants { + private TelemetryConstants() { + + } + + public static final String ATTACHMENTSPROPERTY = "attachments"; + public static final String CHANNELIDPROPERTY = "channelId"; + public static final String CONVERSATIONIDPROPERTY = "conversationId"; + public static final String CONVERSATIONNAMEPROPERTY = "conversationName"; + public static final String DIALOGIDPROPERTY = "dialogId"; + public static final String FROMIDPROPERTY = "fromId"; + public static final String FROMNAMEPROPERTY = "fromName"; + public static final String LOCALEPROPERTY = "locale"; + public static final String RECIPIENTIDPROPERTY = "recipientId"; + public static final String RECIPIENTNAMEPROPERTY = "recipientName"; + public static final String REPLYACTIVITYIDPROPERTY = "replyActivityId"; + public static final String TEXTPROPERTY = "text"; + public static final String SPEAKPROPERTY = "speak"; + public static final String USERIDPROPERTY = "userId"; +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerConstants.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerConstants.java new file mode 100644 index 000000000..705059c20 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerConstants.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * The Telemetry Logger Event names. + */ +public final class TelemetryLoggerConstants { + private TelemetryLoggerConstants() { + + } + + /** + * The name of the event when a new message is received from the user. + */ + public static final String BOTMSGRECEIVEEVENT = "BotMessageReceived"; + + /** + * The name of the event when logged when a message is sent from the bot to the + * user. + */ + public static final String BOTMSGSENDEVENT = "BotMessageSend"; + + /** + * The name of the event when a message is updated by the bot. + */ + public static final String BOTMSGUPDATEEVENT = "BotMessageUpdate"; + + /** + * The name of the event when a message is deleted by the bot. + */ + public static final String BOTMSGDELETEEVENT = "BotMessageDelete"; +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java new file mode 100644 index 000000000..850c69c56 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java @@ -0,0 +1,394 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ResultPair; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.teams.TeamsChannelData; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Middleware for logging incoming, outgoing, updated or deleted Activity + * messages. Uses the {@link BotTelemetryClient} interface. + */ +public class TelemetryLoggerMiddleware implements Middleware { + /** + * Indicates whether determines whether to log personal information that came + * from the user. + */ + private boolean logPersonalInformation; + + /** + * The currently configured {@link BotTelemetryClient} that logs the QnaMessage + * event. + */ + private BotTelemetryClient telemetryClient; + + /** + * Initializes a new instance of the class. + * + * @param withTelemetryClient The IBotTelemetryClient implementation used + * for registering telemetry events. + * @param withLogPersonalInformation TRUE to include personally identifiable + * information. + */ + public TelemetryLoggerMiddleware( + BotTelemetryClient withTelemetryClient, + boolean withLogPersonalInformation + ) { + telemetryClient = + withTelemetryClient == null ? new NullBotTelemetryClient() : withTelemetryClient; + logPersonalInformation = withLogPersonalInformation; + } + + /** + * Gets the currently configured BotTelemetryClient that logs the event. + * + * @return The {@link BotTelemetryClient} being used to log events. + */ + public BotTelemetryClient getTelemetryClient() { + return telemetryClient; + } + + /** + * Logs events based on incoming and outgoing activities using the + * {@link BotTelemetryClient} interface. + * + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("TurnContext")); + } + + // log incoming activity at beginning of turn + return onReceiveActivity(context.getActivity()).thenCompose(receiveResult -> { + // hook up onSend pipeline + context.onSendActivities( + (sendContext, sendActivities, sendNext) -> sendNext.get().thenApply(responses -> { + for (Activity sendActivity : sendActivities) { + onSendActivity(sendActivity); + } + + return responses; + }) + ); + + // hook up update activity pipeline + // @formatter:off + context.onUpdateActivity( + (updateContext, updateActivity, updateNext) -> updateNext.get() + .thenCombine( + onUpdateActivity(updateActivity), (resourceResponse, updateResult) + -> resourceResponse + ) + ); + // @formatter:off + + // hook up delete activity pipeline + context.onDeleteActivity( + (deleteContext, deleteReference, deleteNext) -> deleteNext.get() + .thenCompose(nextResult -> { + Activity deleteActivity = new Activity(ActivityTypes.MESSAGE_DELETE); + deleteActivity.setId(deleteReference.getActivityId()); + deleteActivity.applyConversationReference(deleteReference, false); + + return onDeleteActivity(deleteActivity); + }) + ); + + if (next != null) { + return next.next(); + } + + return CompletableFuture.completedFuture(null); + }); + } + + /** + * Invoked when a message is received from the user. Performs logging of + * telemetry data using the {@link BotTelemetryClient#trackEvent} method. This + * event name used is "BotMessageReceived". + * + * @param activity Current activity sent from user. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onReceiveActivity(Activity activity) { + if (activity == null) { + return CompletableFuture.completedFuture(null); + } + + return fillReceiveEventProperties(activity, null).thenAccept(properties -> { + telemetryClient.trackEvent(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, properties); + }); + } + + /** + * Invoked when the bot sends a message to the user. Performs logging of + * telemetry data using the {@link BotTelemetryClient#trackEvent} method. This + * event name used is "BotMessageSend". + * + * @param activity Current activity sent from user. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onSendActivity(Activity activity) { + return fillSendEventProperties(activity, null).thenAccept(properties -> { + telemetryClient.trackEvent(TelemetryLoggerConstants.BOTMSGSENDEVENT, properties); + }); + } + + /** + * Invoked when the bot updates a message. Performs logging of telemetry data + * using the {@link BotTelemetryClient#trackEvent} method. This event name used + * is "BotMessageUpdate". + * + * @param activity Current activity sent from user. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onUpdateActivity(Activity activity) { + return fillUpdateEventProperties(activity, null).thenAccept(properties -> { + telemetryClient.trackEvent(TelemetryLoggerConstants.BOTMSGUPDATEEVENT, properties); + }); + } + + /** + * Invoked when the bot deletes a message. Performs logging of telemetry data + * using the {@link BotTelemetryClient#trackEvent} method. This event name used + * is "BotMessageDelete". + * + * @param activity Current activity sent from user. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onDeleteActivity(Activity activity) { + return fillDeleteEventProperties(activity, null).thenAccept(properties -> { + telemetryClient.trackEvent(TelemetryLoggerConstants.BOTMSGDELETEEVENT, properties); + }); + } + + /** + * Fills the event properties for the BotMessageReceived. Adheres to the + * LogPersonalInformation flag to filter Name, Text and Speak properties. + * + * @param activity Last activity sent from user. + * @param additionalProperties Additional properties to add to the event. + * @return A dictionary that is sent as "Properties" to + * {@link BotTelemetryClient#trackEvent} method for the + * BotMessageReceived event. + */ + @SuppressWarnings("PMD.EmptyCatchBlock") + protected CompletableFuture> fillReceiveEventProperties( + Activity activity, + Map additionalProperties + ) { + + Map properties = new HashMap(); + String fromId = activity.getFrom().getId() != null ? activity.getFrom().getId() : ""; + properties.put(TelemetryConstants.FROMIDPROPERTY, fromId); + String conversationName = + activity.getConversation().getName() != null ? activity.getConversation().getName() : ""; + properties.put(TelemetryConstants.CONVERSATIONNAMEPROPERTY, conversationName); + String activityLocale = activity.getLocale() != null ? activity.getLocale() : ""; + properties.put(TelemetryConstants.LOCALEPROPERTY, activityLocale); + String recipientId = activity.getRecipient().getId() != null ? activity.getRecipient().getId() : ""; + properties.put(TelemetryConstants.RECIPIENTIDPROPERTY, recipientId); + String recipientName = activity.getRecipient().getName() != null ? activity.getRecipient().getName() : ""; + properties.put(TelemetryConstants.RECIPIENTNAMEPROPERTY, recipientName); + + // Use the LogPersonalInformation flag to toggle logging PII data, text and user + // name are common examples + if (logPersonalInformation) { + if (!StringUtils.isEmpty(activity.getFrom().getName())) { + properties.put(TelemetryConstants.FROMNAMEPROPERTY, activity.getFrom().getName()); + } + + if (!StringUtils.isEmpty(activity.getText())) { + properties.put(TelemetryConstants.TEXTPROPERTY, activity.getText()); + } + + if (!StringUtils.isEmpty(activity.getSpeak())) { + properties.put(TelemetryConstants.SPEAKPROPERTY, activity.getSpeak()); + } + + if (activity.getAttachments() != null && activity.getAttachments().size() > 0) { + try { + properties.put(TelemetryConstants.ATTACHMENTSPROPERTY, + Serialization.toString(activity.getAttachments())); + } catch (JsonProcessingException e) { + } + } + } + + populateAdditionalChannelProperties(activity, properties); + + // Additional Properties can override "stock" properties. + if (additionalProperties != null) { + properties.putAll(additionalProperties); + } + + return CompletableFuture.completedFuture(properties); + } + + /** + * Fills the event properties for BotMessageSend. These properties are logged + * when an activity message is sent by the Bot to the user. + * + * @param activity Last activity sent from user. + * @param additionalProperties Additional properties to add to the event. + * @return A dictionary that is sent as "Properties" to + * {@link BotTelemetryClient#trackEvent} method for the BotMessageSend + * event. + */ + protected CompletableFuture> fillSendEventProperties( + Activity activity, + Map additionalProperties + ) { + + Map properties = new HashMap(); + properties.put(TelemetryConstants.REPLYACTIVITYIDPROPERTY, activity.getReplyToId()); + properties.put(TelemetryConstants.RECIPIENTIDPROPERTY, activity.getRecipient().getId()); + properties.put( + TelemetryConstants.CONVERSATIONNAMEPROPERTY, + activity.getConversation().getName() + ); + properties.put(TelemetryConstants.LOCALEPROPERTY, activity.getLocale()); + + // Use the LogPersonalInformation flag to toggle logging PII data, text and user + // name are common examples + if (logPersonalInformation) { + if (!StringUtils.isEmpty(activity.getRecipient().getName())) { + properties.put( + TelemetryConstants.RECIPIENTNAMEPROPERTY, activity.getRecipient().getName() + ); + } + + if (!StringUtils.isEmpty(activity.getText())) { + properties.put(TelemetryConstants.TEXTPROPERTY, activity.getText()); + } + + if (!StringUtils.isEmpty(activity.getSpeak())) { + properties.put(TelemetryConstants.SPEAKPROPERTY, activity.getSpeak()); + } + } + + // Additional Properties can override "stock" properties. + if (additionalProperties != null) { + properties.putAll(additionalProperties); + } + + return CompletableFuture.completedFuture(properties); + } + + /** + * Fills the event properties for BotMessageUpdate. These properties are logged + * when an activity message is sent by the Bot to the user. + * + * @param activity Last activity sent from user. + * @param additionalProperties Additional properties to add to the event. + * @return A dictionary that is sent as "Properties" to + * {@link BotTelemetryClient#trackEvent} method for the BotMessageUpdate + * event. + */ + protected CompletableFuture> fillUpdateEventProperties( + Activity activity, + Map additionalProperties + ) { + + Map properties = new HashMap(); + properties.put(TelemetryConstants.RECIPIENTIDPROPERTY, activity.getRecipient().getId()); + properties.put(TelemetryConstants.CONVERSATIONIDPROPERTY, activity.getConversation().getId()); + properties.put( + TelemetryConstants.CONVERSATIONNAMEPROPERTY, + activity.getConversation().getName() + ); + properties.put(TelemetryConstants.LOCALEPROPERTY, activity.getLocale()); + + // Use the LogPersonalInformation flag to toggle logging PII data, text is a + // common example + if (logPersonalInformation && !StringUtils.isEmpty(activity.getText())) { + properties.put(TelemetryConstants.TEXTPROPERTY, activity.getText()); + } + + // Additional Properties can override "stock" properties. + if (additionalProperties != null) { + properties.putAll(additionalProperties); + } + + return CompletableFuture.completedFuture(properties); + } + + /** + * Fills the event properties for BotMessageDelete. These properties are logged + * when an activity message is sent by the Bot to the user. + * + * @param activity Last activity sent from user. + * @param additionalProperties Additional properties to add to the event. + * @return A dictionary that is sent as "Properties" to + * {@link BotTelemetryClient#trackEvent} method for the BotMessageDelete + * event. + */ + protected CompletableFuture> fillDeleteEventProperties( + Activity activity, + Map additionalProperties + ) { + + Map properties = new HashMap(); + properties.put(TelemetryConstants.RECIPIENTIDPROPERTY, activity.getRecipient().getId()); + properties.put(TelemetryConstants.CONVERSATIONIDPROPERTY, activity.getConversation().getId()); + properties.put( + TelemetryConstants.CONVERSATIONNAMEPROPERTY, + activity.getConversation().getName() + ); + + // Additional Properties can override "stock" properties. + if (additionalProperties != null) { + properties.putAll(additionalProperties); + } + + return CompletableFuture.completedFuture(properties); + } + + private void populateAdditionalChannelProperties( + Activity activity, + Map properties + ) { + if (StringUtils.equalsIgnoreCase(activity.getChannelId(), Channels.MSTEAMS)) { + ResultPair teamsChannelData = + activity.tryGetChannelData(TeamsChannelData.class); + if (teamsChannelData.result()) { + if (teamsChannelData.value().getTenant() != null) { + properties + .put("TeamsTenantId", teamsChannelData.value().getTenant().getId()); + } + + if (activity.getFrom() != null) { + properties + .put("TeamsUserAadObjectId", activity.getFrom().getAadObjectId()); + } + + try { + if (teamsChannelData.value().getTeam() != null) { + properties.put( + "TeamsTeamInfo", + Serialization.toString(teamsChannelData.value().getTeam()) + ); + } + } catch (JsonProcessingException ignored) { + + } + } + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TraceTranscriptLogger.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TraceTranscriptLogger.java new file mode 100644 index 000000000..73858a0e5 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TraceTranscriptLogger.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +/** + * Represents a transcript logger that writes activities to a + * object. + */ +public class TraceTranscriptLogger implements TranscriptLogger { + /** + * It's a logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(TraceTranscriptLogger.class); + + /** + * For outputting Activity as JSON. + */ + // https://github.com/FasterXML/jackson-databind/wiki/Serialization-Features + private static ObjectMapper mapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + .findAndRegisterModules(); + + /** + * Log an activity to the transcript. + * + * @param activity The activity to transcribe. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture logActivity(Activity activity) { + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity")); + } + + String event = null; + try { + event = mapper.writeValueAsString(activity); + } catch (JsonProcessingException e) { + LOGGER.error("logActivity", e); + CompletableFuture.completedFuture(null); + } + LOGGER.info(event); + + return CompletableFuture.completedFuture(null); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptInfo.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptInfo.java new file mode 100644 index 000000000..218055593 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptInfo.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.time.OffsetDateTime; + +/** + * Represents a copy of a conversation. + */ +public class TranscriptInfo { + private String channelId; + private String id; + private OffsetDateTime created; + + /** + * Constructor. + * + * @param withId The conversation id. + * @param withChannelId The channel id. + * @param withCreated Created timestamp. + */ + public TranscriptInfo(String withId, String withChannelId, OffsetDateTime withCreated) { + id = withId; + channelId = withChannelId; + created = withCreated; + } + + /** + * Gets the ID of the channel in which the conversation occurred. + * + * @return The ID of the channel in which the conversation occurred. + */ + public String channelId() { + return channelId; + } + + /** + * Sets the ID of the channel in which the conversation occurred. + * + * @param withChannelId The ID of the channel in which the conversation + * occurred. + */ + public void setChannelId(String withChannelId) { + channelId = withChannelId; + } + + /** + * Gets the ID of the conversation. + * + * @return The ID of the conversation. + */ + public String getId() { + return id; + } + + /** + * Sets the ID of the conversation. + * + * @param withId The ID of the conversation. + */ + public void setId(String withId) { + id = withId; + } + + /** + * Gets the date the conversation began. + * + * @return The date then conversation began. + */ + public OffsetDateTime getCreated() { + return created; + } + + /** + * Sets the date the conversation began. + * + * @param withCreated The date then conversation began. + */ + public void setCreated(OffsetDateTime withCreated) { + created = withCreated; + } +} diff --git a/libraries/botbuilder/src/main/java/com/microsoft/bot/builder/TranscriptLogger.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptLogger.java similarity index 82% rename from libraries/botbuilder/src/main/java/com/microsoft/bot/builder/TranscriptLogger.java rename to libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptLogger.java index 0264f41d0..4403da710 100644 --- a/libraries/botbuilder/src/main/java/com/microsoft/bot/builder/TranscriptLogger.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptLogger.java @@ -1,11 +1,9 @@ -package com.microsoft.bot.builder; - - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +package com.microsoft.bot.builder; -import com.microsoft.bot.schema.models.Activity; +import com.microsoft.bot.schema.Activity; import java.util.concurrent.CompletableFuture; @@ -19,5 +17,5 @@ public interface TranscriptLogger { * @param activity The activity to transcribe. * @return A task that represents the work queued to execute. */ - void LogActivityAsync(Activity activity); + CompletableFuture logActivity(Activity activity); } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptLoggerMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptLoggerMiddleware.java new file mode 100644 index 000000000..8a0d020c2 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptLoggerMiddleware.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityEventNames; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.RoleTypes; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.apache.commons.lang3.StringUtils; + +/** + * When added, this middleware will log incoming and outgoing activities to a + * TranscriptStore. + */ +public class TranscriptLoggerMiddleware implements Middleware { + + /** + * The TranscriptLogger to log to. + */ + private TranscriptLogger transcriptLogger; + + /** + * Activity queue. + */ + private Queue transcript = new ConcurrentLinkedQueue(); + + /** + * Initializes a new instance of the + * class. + * + * @param withTranscriptLogger The transcript logger to use. + */ + public TranscriptLoggerMiddleware(TranscriptLogger withTranscriptLogger) { + if (withTranscriptLogger == null) { + throw new IllegalArgumentException( + "TranscriptLoggerMiddleware requires a ITranscriptLogger implementation." + ); + } + + transcriptLogger = withTranscriptLogger; + } + + /** + * Records incoming and outgoing activities to the conversation store. + * + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + // log incoming activity at beginning of turn + if (context.getActivity() != null) { + if (context.getActivity().getFrom() == null) { + context.getActivity().setFrom(new ChannelAccount()); + } + + if (context.getActivity().getFrom().getProperties().get("role") == null + && context.getActivity().getFrom().getRole() == null + ) { + context.getActivity().getFrom().setRole(RoleTypes.USER); + } + + // We should not log ContinueConversation events used by skills to initialize the middleware. + if (!(context.getActivity().isType(ActivityTypes.EVENT) + && StringUtils.equals(context.getActivity().getName(), ActivityEventNames.CONTINUE_CONVERSATION)) + ) { + logActivity(Activity.clone(context.getActivity()), true); + } + } + + // hook up onSend pipeline + context.onSendActivities( + (ctx, activities, nextSend) -> { + // run full pipeline + return nextSend.get().thenApply(responses -> { + for (Activity activity : activities) { + logActivity(Activity.clone(activity), false); + } + + return responses; + }); + } + ); + + // hook up update activity pipeline + context.onUpdateActivity( + (ctx, activity, nextUpdate) -> { + // run full pipeline + return nextUpdate.get().thenApply(resourceResponse -> { + // add Message Update activity + Activity updateActivity = Activity.clone(activity); + updateActivity.setType(ActivityTypes.MESSAGE_UPDATE); + logActivity(updateActivity, false); + + return resourceResponse; + }); + } + ); + + // hook up delete activity pipeline + context.onDeleteActivity( + (ctx, reference, nextDel) -> { + // run full pipeline + return nextDel.get().thenApply(nextDelResult -> { + // add MessageDelete activity + // log as MessageDelete activity + Activity deleteActivity = new Activity(ActivityTypes.MESSAGE_DELETE); + deleteActivity.setId(reference.getActivityId()); + deleteActivity.applyConversationReference(reference, false); + + logActivity(deleteActivity, false); + + return null; + }); + } + ); + + // process bot logic + return next.next() + .thenAccept( + nextResult -> { + // flush transcript at end of turn + while (!transcript.isEmpty()) { + Activity activity = transcript.poll(); + transcriptLogger.logActivity(activity); + } + } + ); + } + + private void logActivity(Activity activity, boolean incoming) { + if (activity.getTimestamp() == null) { + activity.setTimestamp(OffsetDateTime.now(ZoneId.of("UTC"))); + } + + if (activity.getFrom() == null) { + activity.setFrom(new ChannelAccount()); + } + + if (activity.getFrom().getRole() == null) { + activity.getFrom().setRole(incoming ? RoleTypes.USER : RoleTypes.BOT); + } + + transcript.offer(activity); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptStore.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptStore.java new file mode 100644 index 000000000..38579d546 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TranscriptStore.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; + +import java.time.OffsetDateTime; +import java.util.concurrent.CompletableFuture; + +/** + * Transcript logger stores activities for conversations for recall. + */ +public interface TranscriptStore extends TranscriptLogger { + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @return A task that represents the work queued to execute. If the task + * completes successfully, the result contains the matching activities. + */ + default CompletableFuture> getTranscriptActivities( + String channelId, + String conversationId + ) { + return getTranscriptActivities(channelId, conversationId, null); + } + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @param continuationToken The continuation token (if available). + * @return A task that represents the work queued to execute. If the task + * completes successfully, the result contains the matching activities. + */ + default CompletableFuture> getTranscriptActivities( + String channelId, + String conversationId, + String continuationToken + ) { + return getTranscriptActivities(channelId, conversationId, continuationToken, null); + } + + /** + * Gets from the store activities that match a set of criteria. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation. + * @param continuationToken The continuation token (if available). + * @param startDate A cutoff date. Activities older than this date are + * not included. + * @return A task that represents the work queued to execute. If the task + * completes successfully, the result contains the matching activities. + */ + CompletableFuture> getTranscriptActivities( + String channelId, + String conversationId, + String continuationToken, + OffsetDateTime startDate + ); + + /** + * Gets the conversations on a channel from the store. + * + * @param channelId The ID of the channel. + * @return A task that represents the work queued to execute. + */ + default CompletableFuture> listTranscripts(String channelId) { + return listTranscripts(channelId, null); + } + + /** + * Gets the conversations on a channel from the store. + * + * @param channelId The ID of the channel. + * @param continuationToken The continuation token (if available). + * @return A task that represents the work queued to execute. + */ + CompletableFuture> listTranscripts( + String channelId, + String continuationToken + ); + + /** + * Deletes conversation data from the store. + * + * @param channelId The ID of the channel the conversation is in. + * @param conversationId The ID of the conversation to delete. + * @return A task that represents the work queued to execute. + */ + CompletableFuture deleteTranscript(String channelId, String conversationId); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java new file mode 100644 index 000000000..7db509d7c --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContext.java @@ -0,0 +1,321 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Provides context for a turn of a bot. + * + *

+ * Context provides information needed to process an incoming activity. The + * context object is created by a {@link BotAdapter} and persists for the length + * of the turn. + *

+ * + * {@link Bot} {@link Middleware} + */ +public interface TurnContext { + String STATE_TURN_LOCALE = "turn.locale"; + + /** + * Sends a trace activity to the {@link BotAdapter} for logging purposes. + * + * @param turnContext The context for the current turn. + * @param name The value to assign to the activity's + * {@link Activity#getName} property. + * @param value The value to assign to the activity's + * {@link Activity#getValue} property. + * @param valueType The value to assign to the activity's + * {@link Activity#getValueType} property. + * @param label The value to assign to the activity's + * {@link Activity#getLabel} property. + * @return A task that represents the work queued to execute. If the adapter is + * being hosted in the Emulator, the task result contains a + * {@link ResourceResponse} object with the original trace activity's + * ID; otherwise, it contains a {@link ResourceResponse} object + * containing the ID that the receiving channel assigned to the + * activity. + */ + static CompletableFuture traceActivity( + TurnContext turnContext, + String name, + Object value, + String valueType, + String label + ) { + return turnContext + .sendActivity(turnContext.getActivity().createTrace(name, value, valueType, label)); + } + + /** + * @param turnContext The turnContext. + * @param name The name of the activity. + * @return A future with the ResourceReponse. + */ + static CompletableFuture traceActivity(TurnContext turnContext, String name) { + return traceActivity(turnContext, name, null, null, null); + } + + /** + * Gets the locale on this context object. + * @return The string of locale on this context object. + */ + String getLocale(); + + /** + * Set the locale on this context object. + * @param withLocale The string of locale on this context object. + */ + void setLocale(String withLocale); + + /** + * Gets the bot adapter that created this context object. + * + * @return The bot adapter that created this context object. + */ + BotAdapter getAdapter(); + + /** + * Gets the collection of values cached with the context object for the lifetime + * of the turn. + * + * @return The collection of services registered on this context object. + */ + TurnContextStateCollection getTurnState(); + + /** + * Gets the activity for this turn of the bot. + * + * @return The activity for this turn of the bot. + */ + Activity getActivity(); + + /** + * Gets a value indicating whether at least one response was sent for the + * current turn. + * + * @return {@code true} if at least one response was sent for the current turn; + * otherwise, {@code false}. + */ + boolean getResponded(); + + /** + * Sends a message activity to the sender of the incoming activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}. + *

+ * + * @param textReplyToSend The text of the message to send. + * @return A task that represents the work queued to execute. + */ + CompletableFuture sendActivity(String textReplyToSend); + + /** + * Sends a message activity to the sender of the incoming activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}. + *

+ * + *

+ * To control various characteristics of your bot's speech such as voice, rate, + * volume, pronunciation, and pitch, specify {@code speak} in Speech Synthesis + * Markup Language (SSML) format. + *

+ * + * @param textReplyToSend The text of the message to send. + * @param speak Optional, text to be spoken by your bot on a + * speech-enabled channel. + * @return A task that represents the work queued to execute. + */ + CompletableFuture sendActivity(String textReplyToSend, String speak); + + /** + * Sends a message activity to the sender of the incoming activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}. + *

+ * + *

+ * To control various characteristics of your bot's speech such as voice, rate, + * volume, pronunciation, and pitch, specify {@code speak} in Speech Synthesis + * Markup Language (SSML) format. + *

+ * + * @param textReplyToSend The text of the message to send. + * @param speak Optional, text to be spoken by your bot on a + * speech-enabled channel. + * @param inputHint Optional, indicates whether your bot is accepting, + * expecting, or ignoring user input after the message is + * delivered to the client. One of: "acceptingInput", + * "ignoringInput", or "expectingInput". Default is + * "acceptingInput". + * @return A task that represents the work queued to execute. + */ + CompletableFuture sendActivity( + String textReplyToSend, + String speak, + InputHints inputHint + ); + + /** + * Sends an activity to the sender of the incoming activity. + * + * @param activity The activity to send. + * @return A task that represents the work queued to execute. If the activity is + * successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving + * channel assigned to the activity. + */ + CompletableFuture sendActivity(Activity activity); + + /** + * Sends an Activity to the sender of the incoming Activity without returning a + * ResourceResponse. + * + * @param activity The activity to send. + * @return A task that represents the work queued to execute. + */ + default CompletableFuture sendActivityBlind(Activity activity) { + return sendActivity(activity).thenApply(aVoid -> null); + } + + /** + * Sends a list of activities to the sender of the incoming activity. + * + *

+ * If the activities are successfully sent, the task result contains an array of + * {@link ResourceResponse} objects containing the IDs that the receiving + * channel assigned to the activities. + *

+ * + * @param activities The activities to send. + * @return A task that represents the work queued to execute. + */ + CompletableFuture sendActivities(List activities); + + /** + * Helper method to send an array of Activities. This calls + * {@link #sendActivities(List)}. + * + * @param activities The array of activities. + * @return A task that represents the work queued to execute. + */ + default CompletableFuture sendActivities(Activity... activities) { + return sendActivities(Arrays.asList(activities)); + } + + /** + * Replaces an existing activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * Before calling this, set the ID of the replacement activity to the ID of the + * activity to replace. + *

+ * + * @param withActivity New replacement activity. + * @return A task that represents the work queued to execute. + */ + CompletableFuture updateActivity(Activity withActivity); + + /** + * Deletes an existing activity. + * + * @param activityId The ID of the activity to delete. + * @return A task that represents the work queued to execute. + */ + CompletableFuture deleteActivity(String activityId); + + /** + * Deletes an existing activity. + * + * @param conversationReference The conversation containing the activity to + * delete. + * @return A task that represents the work queued to execute. The conversation + * reference's {@link ConversationReference#getActivityId} indicates the + * activity in the conversation to delete. + */ + CompletableFuture deleteActivity(ConversationReference conversationReference); + + /** + * Adds a response handler for send activity operations. + * + *

+ * When the context's {@link #sendActivity(Activity)} or + * {@link #sendActivities(List)} methods are called, the adapter calls the + * registered handlers in the order in which they were added to the context + * object. + *

+ * + * @param handler The handler to add to the context object. + * @return The updated context object. + */ + TurnContext onSendActivities(SendActivitiesHandler handler); + + /** + * Adds a response handler for update activity operations. + * + *

+ * When the context's {@link #updateActivity(Activity)} is called, the adapter + * calls the registered handlers in the order in which they were added to the + * context object. + *

+ * + * @param handler The handler to add to the context object. + * @return The updated context object. + */ + TurnContext onUpdateActivity(UpdateActivityHandler handler); + + /** + * Adds a response handler for delete activity operations. + * + *

+ * When the context's {@link #deleteActivity(String)} is called, the adapter + * calls the registered handlers in the order in which they were added to the + * context object. + *

+ * + * @param handler The handler to add to the context object. + * @return The updated context object. + * @throws NullPointerException {@code handler} is {@code null}. + */ + TurnContext onDeleteActivity(DeleteActivityHandler handler); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java new file mode 100644 index 000000000..1a513a793 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java @@ -0,0 +1,624 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.bot.schema.ResourceResponse; +import java.util.Locale; +import org.apache.commons.lang3.LocaleUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Provides context for a turn of a bot. Context provides information needed to + * process an incoming activity. The context object is created by a + * {@link BotAdapter} and persists for the length of the turn. {@link Bot} + * {@link Middleware} + */ +public class TurnContextImpl implements TurnContext, AutoCloseable { + /** + * The bot adapter that created this context object. + */ + private final BotAdapter adapter; + + /** + * The activity associated with this turn; or null when processing a proactive + * message. + */ + private final Activity activity; + + private List bufferedReplyActivities = new ArrayList<>(); + + /** + * Response handlers for send activity operations. + */ + private final List onSendActivities = new ArrayList<>(); + + /** + * Response handlers for update activity operations. + */ + private final List onUpdateActivity = new ArrayList<>(); + + /** + * Response handlers for delete activity operations. + */ + private final List onDeleteActivity = new ArrayList<>(); + + /** + * The services registered on this context object. + */ + private final TurnContextStateCollection turnState; + + /** + * Indicates whether at least one response was sent for the current turn. + */ + private Boolean responded = false; + + /** + * Creates a context object. + * + * @param withAdapter The adapter creating the context. + * @param withActivity The incoming activity for the turn; or {@code null} for a + * turn for a proactive message. + * @throws IllegalArgumentException {@code activity} or {@code adapter} is + * {@code null}. For use by bot adapter + * implementations only. + */ + public TurnContextImpl(BotAdapter withAdapter, Activity withActivity) { + if (withAdapter == null) { + throw new IllegalArgumentException("adapter"); + } + adapter = withAdapter; + + if (withActivity == null) { + throw new IllegalArgumentException("activity"); + } + activity = withActivity; + + turnState = new TurnContextStateCollection(); + } + + /** + * Adds a response handler for send activity operations. + * + * @param handler The handler to add to the context object. + * @return The updated context object. + * @throws IllegalArgumentException {@code handler} is {@code null}. When the + * context's {@link #sendActivity(Activity)} or + * {@link #sendActivities(List)} methods are + * called, the adapter calls the registered + * handlers in the order in which they were + * added to the context object. + */ + @Override + public TurnContext onSendActivities(SendActivitiesHandler handler) { + if (handler == null) { + throw new IllegalArgumentException("handler"); + } + + onSendActivities.add(handler); + return this; + } + + /** + * Adds a response handler for update activity operations. + * + * @param handler The handler to add to the context object. + * @return The updated context object. + * @throws IllegalArgumentException {@code handler} is {@code null}. When the + * context's {@link #updateActivity(Activity)} + * is called, the adapter calls the registered + * handlers in the order in which they were + * added to the context object. + */ + @Override + public TurnContext onUpdateActivity(UpdateActivityHandler handler) { + if (handler == null) { + throw new IllegalArgumentException("handler"); + } + + onUpdateActivity.add(handler); + return this; + } + + /** + * Adds a response handler for delete activity operations. + * + * @param handler The handler to add to the context object. + * @return The updated context object. + * @throws IllegalArgumentException {@code handler} is {@code null}. When the + * context's {@link #deleteActivity(String)} is + * called, the adapter calls the registered + * handlers in the order in which they were + * added to the context object. + */ + @Override + public TurnContext onDeleteActivity(DeleteActivityHandler handler) { + if (handler == null) { + throw new IllegalArgumentException("handler"); + } + + onDeleteActivity.add(handler); + return this; + } + + /** + * Gets the bot adapter that created this context object. + * + * @return The BotAdaptor for this turn. + */ + public BotAdapter getAdapter() { + return this.adapter; + } + + /** + * Gets the services registered on this context object. + * + * @return the TurnContextStateCollection for this turn. + */ + public TurnContextStateCollection getTurnState() { + return this.turnState; + } + + /** + * Gets the activity associated with this turn; or {@code null} when processing + * a proactive message. + */ + @Override + public Activity getActivity() { + return this.activity; + } + + /** + * Indicates whether at least one response was sent for the current turn. + * + * @return {@code true} if at least one response was sent for the current turn. + */ + @Override + public boolean getResponded() { + return responded; + } + + /** + * Gets the locale on this context object. + * @return The string of locale on this context object. + */ + @Override + public String getLocale() { + return getTurnState().get(STATE_TURN_LOCALE); + } + + /** + * Set the locale on this context object. + * @param withLocale The string of locale on this context object. + */ + @Override + public void setLocale(String withLocale) { + if (StringUtils.isEmpty(withLocale)) { + getTurnState().remove(STATE_TURN_LOCALE); + } else if ( + LocaleUtils.isAvailableLocale(new Locale.Builder().setLanguageTag(withLocale).build()) + ) { + getTurnState().replace(STATE_TURN_LOCALE, withLocale); + } else { + getTurnState().replace(STATE_TURN_LOCALE, Locale.ENGLISH.getCountry()); + } + } + + /** + * Gets a list of activities to send when `context.Activity.DeliveryMode == 'expectReplies'. + * @return A list of activities. + */ + public List getBufferedReplyActivities() { + return bufferedReplyActivities; + } + + /** + * Sends a message activity to the sender of the incoming activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}. + *

+ * + * @param textReplyToSend The text of the message to send. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or + * whitespace. + */ + @Override + public CompletableFuture sendActivity(String textReplyToSend) { + return sendActivity(textReplyToSend, null, null); + } + + /** + * Sends a message activity to the sender of the incoming activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}. + *

+ * + * @param textReplyToSend The text of the message to send. + * @param speak To control various characteristics of your bot's + * speech such as voice rate, volume, pronunciation, and + * pitch, specify Speech Synthesis Markup Language (SSML) + * format. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or + * whitespace. + */ + @Override + public CompletableFuture sendActivity(String textReplyToSend, String speak) { + return sendActivity(textReplyToSend, speak, null); + } + + /** + * Sends a message activity to the sender of the incoming activity. + * + *

+ * If the activity is successfully sent, the task result contains a + * {@link ResourceResponse} object containing the ID that the receiving channel + * assigned to the activity. + *

+ * + *

+ * See the channel's documentation for limits imposed upon the contents of + * {@code textReplyToSend}. + *

+ * + * @param textReplyToSend The text of the message to send. + * @param speak To control various characteristics of your bot's + * speech such as voice rate, volume, pronunciation, and + * pitch, specify Speech Synthesis Markup Language (SSML) + * format. + * @param inputHint (Optional) Input hint. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or + * whitespace. + */ + @Override + public CompletableFuture sendActivity( + String textReplyToSend, + String speak, + InputHints inputHint + ) { + if (StringUtils.isEmpty(textReplyToSend)) { + return Async.completeExceptionally(new IllegalArgumentException("textReplyToSend")); + } + + Activity activityToSend = new Activity(ActivityTypes.MESSAGE); + activityToSend.setText(textReplyToSend); + + if (StringUtils.isNotEmpty(speak)) { + activityToSend.setSpeak(speak); + } + + if (inputHint != null) { + activityToSend.setInputHint(inputHint); + } + + return sendActivity(activityToSend); + } + + /** + * Sends an activity to the sender of the incoming activity. + * + * @param activityToSend The activity to send. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException {@code activity} is {@code null}. If the + * activity is successfully sent, the task + * result contains a {@link ResourceResponse} + * object containing the ID that the receiving + * channel assigned to the activity. + */ + @Override + public CompletableFuture sendActivity(Activity activityToSend) { + if (activityToSend == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity")); + } + + return sendActivities(Collections.singletonList(activityToSend)) + .thenApply(resourceResponses -> { + if (resourceResponses == null || resourceResponses.length == 0) { + // It's possible an interceptor prevented the activity from having been sent. + // Just return an empty response in that case. + return new ResourceResponse(); + } + return resourceResponses[0]; + }); + } + + /** + * Sends a set of activities to the sender of the incoming activity. + * + * @param activities The activities to send. + * @return A task that represents the work queued to execute. If the activities + * are successfully sent, the task result contains an array of + * {@link ResourceResponse} objects containing the IDs that the + * receiving channel assigned to the activities. + */ + @Override + public CompletableFuture sendActivities(List activities) { + if (activities == null || activities.size() == 0) { + return Async.completeExceptionally(new IllegalArgumentException("activities")); + } + + // Bind the relevant Conversation Reference properties, such as URLs and + // ChannelId's, to the activities we're about to send. + ConversationReference cr = activity.getConversationReference(); + + // Buffer the incoming activities into a List since we allow the set to be + // manipulated by the callbacks + // Bind the relevant Conversation Reference properties, such as URLs and + // ChannelId's, to the activity we're about to send + List bufferedActivities = activities.stream() + .map(a -> a.applyConversationReference(cr)) + .collect(Collectors.toList()); + + if (onSendActivities.size() == 0) { + return sendActivitiesThroughAdapter(bufferedActivities); + } + + return sendActivitiesThroughCallbackPipeline(bufferedActivities, 0); + } + + private CompletableFuture sendActivitiesThroughAdapter( + List activities + ) { + if (DeliveryModes.fromString(getActivity().getDeliveryMode()) == DeliveryModes.EXPECT_REPLIES) { + ResourceResponse[] responses = new ResourceResponse[activities.size()]; + boolean sentNonTraceActivity = false; + + for (int index = 0; index < responses.length; index++) { + Activity sendActivity = activities.get(index); + bufferedReplyActivities.add(sendActivity); + + // Ensure the TurnState has the InvokeResponseKey, since this activity + // is not being sent through the adapter, where it would be added to TurnState. + if (activity.isType(ActivityTypes.INVOKE_RESPONSE)) { + getTurnState().add(BotFrameworkAdapter.INVOKE_RESPONSE_KEY, activity); + } + + responses[index] = new ResourceResponse(); + sentNonTraceActivity |= !sendActivity.isType(ActivityTypes.TRACE); + } + + if (sentNonTraceActivity) { + responded = true; + } + + return CompletableFuture.completedFuture(responses); + } else { + return adapter.sendActivities(this, activities).thenApply(responses -> { + boolean sentNonTraceActivity = false; + + for (int index = 0; index < responses.length; index++) { + Activity sendActivity = activities.get(index); + sendActivity.setId(responses[index].getId()); + sentNonTraceActivity |= !sendActivity.isType(ActivityTypes.TRACE); + } + + if (sentNonTraceActivity) { + responded = true; + } + + return responses; + }); + } + } + + private CompletableFuture sendActivitiesThroughCallbackPipeline( + List activities, + int nextCallbackIndex + ) { + if (nextCallbackIndex == onSendActivities.size()) { + return sendActivitiesThroughAdapter(activities); + } + + return onSendActivities.get(nextCallbackIndex) + .invoke( + this, activities, + () -> sendActivitiesThroughCallbackPipeline(activities, nextCallbackIndex + 1) + ); + } + + /** + * Replaces an existing activity. + * + * @param withActivity New replacement activity. + * @return A task that represents the work queued to execute. + * @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP + * operation + * failed and + * the response + * contained + * additional + * information. + */ + @Override + public CompletableFuture updateActivity(Activity withActivity) { + if (withActivity == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity")); + } + + ConversationReference conversationReference = activity.getConversationReference(); + withActivity.applyConversationReference(conversationReference); + + Supplier> actuallyUpdateStuff = () -> getAdapter() + .updateActivity(this, withActivity); + + return updateActivityInternal( + withActivity, onUpdateActivity.iterator(), actuallyUpdateStuff + ); + } + + private CompletableFuture updateActivityInternal( + Activity updateActivity, + Iterator updateHandlers, + Supplier> callAtBottom + ) { + if (updateActivity == null) { + return Async.completeExceptionally(new IllegalArgumentException("Activity")); + } + if (updateHandlers == null) { + return Async.completeExceptionally(new IllegalArgumentException("updateHandlers")); + } + + // No middleware to run. + if (!updateHandlers.hasNext()) { + if (callAtBottom != null) { + return callAtBottom.get(); + } + return CompletableFuture.completedFuture(null); + } + + // Default to "No more Middleware after this". + Supplier> next = () -> { + // Remove the first item from the list of middleware to call, + // so that the next call just has the remaining items to worry about. + if (updateHandlers.hasNext()) { + updateHandlers.next(); + } + + return updateActivityInternal(updateActivity, updateHandlers, callAtBottom) + .thenApply(resourceResponse -> { + updateActivity.setId(resourceResponse.getId()); + return resourceResponse; + }); + }; + + // Grab the current middleware, which is the 1st element in the array, and + // execute it + UpdateActivityHandler toCall = updateHandlers.next(); + return toCall.invoke(this, updateActivity, next); + } + + /** + * Deletes an existing activity. + * + * @param activityId The ID of the activity to delete. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture deleteActivity(String activityId) { + if (StringUtils.isWhitespace(activityId) || StringUtils.isEmpty(activityId)) { + return Async.completeExceptionally(new IllegalArgumentException("activityId")); + } + + ConversationReference cr = activity.getConversationReference(); + cr.setActivityId(activityId); + + Supplier> actuallyDeleteStuff = () -> getAdapter() + .deleteActivity(this, cr); + + return deleteActivityInternal(cr, onDeleteActivity.iterator(), actuallyDeleteStuff); + } + + /** + * Deletes an existing activity. + * + * The conversation reference's {@link ConversationReference#getActivityId} + * indicates the activity in the conversation to delete. + * + * @param conversationReference The conversation containing the activity to + * delete. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture deleteActivity(ConversationReference conversationReference) { + if (conversationReference == null) { + return Async.completeExceptionally(new IllegalArgumentException("conversationReference")); + } + + Supplier> actuallyDeleteStuff = () -> getAdapter() + .deleteActivity(this, conversationReference); + + return deleteActivityInternal( + conversationReference, onDeleteActivity.iterator(), actuallyDeleteStuff + ); + } + + private CompletableFuture deleteActivityInternal( + ConversationReference cr, + Iterator deleteHandlers, + Supplier> callAtBottom + ) { + if (cr == null) { + return Async.completeExceptionally(new IllegalArgumentException("ConversationReference")); + } + if (deleteHandlers == null) { + return Async.completeExceptionally(new IllegalArgumentException("deleteHandlers")); + } + + // No middleware to run. + if (!deleteHandlers.hasNext()) { + if (callAtBottom != null) { + return callAtBottom.get(); + } + return CompletableFuture.completedFuture(null); + } + + // Default to "No more Middleware after this". + Supplier> next = () -> { + // Remove the first item from the list of middleware to call, + // so that the next call just has the remaining items to worry about. + if (deleteHandlers.hasNext()) { + deleteHandlers.next(); + } + + return deleteActivityInternal(cr, deleteHandlers, callAtBottom); + }; + + // Grab the current middleware, which is the 1st element in the array, and + // execute it. + DeleteActivityHandler toCall = deleteHandlers.next(); + return toCall.invoke(this, cr, next); + } + + /** + * Auto call of {@link #close}. + */ + @Override + public void finalize() { + try { + close(); + } catch (Exception ignored) { + + } + } + + /** + * AutoClosable#close. + * + * @throws Exception If the TurnContextStateCollection. + */ + @Override + public void close() throws Exception { + turnState.close(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java new file mode 100644 index 000000000..f42e8b5f4 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.ConnectorClient; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a set of collection of services associated with the + * {@link TurnContext}. + */ +public class TurnContextStateCollection implements AutoCloseable { + /** + * Map of objects managed by this class. + */ + private Map state = new HashMap<>(); + + /** + * Get a value. + * + * @param key The key. + * @param The type of the value. + * @return The value. + * @throws IllegalArgumentException Null key. + */ + public T get(String key) throws IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("key"); + } + + Object service = state.get(key); + try { + return (T) service; + } catch (ClassCastException e) { + return null; + } + } + + /** + * Returns the Services stored in the TurnContextStateCollection. + * @return the Map of String, Object pairs that contains the names and services for this collection. + */ + public Map getTurnStateServices() { + return state; + } + + + /** + * Get a service by type using its full type name as the key. + * + * @param type The type of service to be retrieved. This will use the value + * returned by Class.getName as the key. + * @param The type of the value. + * @return The service stored under the specified key. + */ + public T get(Class type) { + return get(type.getName()); + } + + /** + * Adds a value to the turn's context. + * + * @param key The name of the value. + * @param value The value to add. + * @param The type of the value. + * @throws IllegalArgumentException For null key or value. + */ + public void add(String key, T value) throws IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("key"); + } + + if (value == null) { + throw new IllegalArgumentException("value"); + } + + if (state.containsKey(key)) { + throw new IllegalArgumentException(String.format("Key %s already exists", key)); + } + + state.put(key, value); + } + + /** + * Add a service using its type name ({@link Class#getName()} as the key. + * + * @param value The service to add. + * @param The type of the value. + * @throws IllegalArgumentException For null value. + */ + public void add(T value) throws IllegalArgumentException { + if (value == null) { + throw new IllegalArgumentException("value"); + } + + add(value.getClass().getName(), value); + } + + /** + * Removes a value. + * + * @param key The name of the value to remove. + */ + public void remove(String key) { + state.remove(key); + } + + /** + * Replaces a value. + * + * @param key The name of the value to replace. + * @param value The new value. + */ + public void replace(String key, Object value) { + state.remove(key); + add(key, value); + } + + /** + * Replaces a value. + * @param value The service to add. + * @param The type of the value. + */ + public void replace(T value) { + String key = value.getClass().getName(); + replace(key, value); + } + + /** + * Returns true if this contains a mapping for the specified + * key. + * @param key The name of the value. + * @return True if the key exists. + */ + public boolean containsKey(String key) { + return state.containsKey(key); + } + + /** + * Auto call of {@link #close}. + */ + @Override + public void finalize() { + try { + close(); + } catch (Exception ignored) { + + } + } + + /** + * Close all contained {@link AutoCloseable} values. + * + * @throws Exception Exceptions encountered by children during close. + */ + @Override + public void close() throws Exception { + for (Map.Entry entry : state.entrySet()) { + if (entry.getValue() instanceof AutoCloseable) { + if (entry.getValue() instanceof ConnectorClient) { + continue; + } + ((AutoCloseable) entry.getValue()).close(); + } + } + } + + /** + * Copy the values from another TurnContextStateCollection. + * @param other The collection to copy. + */ + public void copy(TurnContextStateCollection other) { + if (other != null) { + for (String key : other.state.keySet()) { + state.put(key, other.state.get(key)); + } + } + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnStateConstants.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnStateConstants.java new file mode 100644 index 000000000..fab1f8a14 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnStateConstants.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder; + +import java.time.Duration; + +/** + * Constants used in TurnState. + */ +public final class TurnStateConstants { + + private TurnStateConstants() { + + } + + /** + * TurnState key for the OAuth login timeout. + */ + public static final String OAUTH_LOGIN_TIMEOUT_KEY = "loginTimeout"; + + /** + * Name of the token polling settings key. + */ + public static final String TOKEN_POLLING_SETTINGS_KEY = "tokenPollingSettings"; + + /** + * Default amount of time an OAuthCard will remain active (clickable and + * actively waiting for a token). After this time: (1) the OAuthCard will not + * allow the user to click on it. (2) any polling triggered by the OAuthCard + * will stop. + */ + public static final Duration OAUTH_LOGIN_TIMEOUT_VALUE = Duration.ofMinutes(15); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java new file mode 100644 index 000000000..7ac36c695 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +/** + * Tuple class containing an HTTP Status Code and a JSON Serializable object. + * The HTTP Status code is, in the invoke activity scenario, what will be set in + * the resulting POST. The Body of the resulting POST will be the JSON + * Serialized content from the Body property. + * @param The type for the body of the TypedInvokeResponse. + */ +public class TypedInvokeResponse extends InvokeResponse { + + /** + * Initializes new instance of InvokeResponse. + * + * @param withStatus The invoke response status. + * @param withBody The invoke response body. + */ + public TypedInvokeResponse(int withStatus, T withBody) { + super(withStatus, withBody); + } + + /** + * Sets the body with a typed value. + * @param withBody the typed value to set the body to. + */ + public void setTypedBody(T withBody) { + super.setBody(withBody); + } + + /** + * Gets the body content for the response. + * + * @return The body content. + */ + public T getTypedBody() { + return (T) super.getBody(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UpdateActivityHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UpdateActivityHandler.java new file mode 100644 index 000000000..3ff3417ea --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UpdateActivityHandler.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * A method that can participate in update activity events for the current turn. + */ +@FunctionalInterface +public interface UpdateActivityHandler { + /** + * A method that can participate in update activity events for the current turn. + * + * @param context The context object for the turn. + * @param activity The replacement activity. + * @param next The delegate to call to continue event processing. + * @return A task that represents the work queued to execute. A handler calls + * the {@code next} delegate to pass control to the next registered + * handler. If a handler doesn’t call the next delegate, the adapter + * does not call any of the subsequent handlers and does not update the + * activity. + *

+ * The activity's {@link Activity#getId} indicates the activity in the + * conversation to replace. + *

+ *

+ * If the activity is successfully sent, the + * delegate returns a {@link ResourceResponse} object containing the ID + * that the receiving channel assigned to the activity. Use this + * response object as the return value of this handler. + *

+ */ + CompletableFuture invoke( + TurnContext context, + Activity activity, + Supplier> next + ); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UserState.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UserState.java new file mode 100644 index 000000000..1aab8bdec --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UserState.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.apache.commons.lang3.StringUtils; + +/** + * Handles persistence of a user state object using the user ID as part of the + * key. + */ +public class UserState extends BotState { + /** + * Creates a new {@link UserState} object. + * + * @param withStorage The storage provider to use. + */ + public UserState(Storage withStorage) { + super(withStorage, UserState.class.getSimpleName()); + } + + /** + * Gets the user key to use when reading and writing state to and from storage. + * + * @param turnContext The context object for this turn. + * @return The key for the channel and sender. + */ + @Override + public String getStorageKey(TurnContext turnContext) throws IllegalArgumentException { + if (turnContext.getActivity() == null) { + throw new IllegalArgumentException("invalid activity"); + } + + if (StringUtils.isEmpty(turnContext.getActivity().getChannelId())) { + throw new IllegalArgumentException("invalid activity-missing channelId"); + } + + if ( + turnContext.getActivity().getFrom() == null + || StringUtils.isEmpty(turnContext.getActivity().getFrom().getId()) + ) { + throw new IllegalArgumentException("invalid activity-missing From.Id"); + } + + // {channelId}/users/{fromId} + return turnContext.getActivity().getChannelId() + "/users/" + + turnContext.getActivity().getFrom().getId(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UserTokenProvider.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UserTokenProvider.java new file mode 100644 index 000000000..995b97a8d --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/UserTokenProvider.java @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * OAuth provider. + */ +public interface UserTokenProvider { + /** + * Attempts to retrieve the token for a user that's in a login flow. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param magicCode (Optional) Optional user entered code to validate. + * @return Token Response. + */ + CompletableFuture getUserToken( + TurnContext turnContext, + String connectionName, + String magicCode + ); + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @return A task that represents the work queued to execute. If the task + * completes successfully, the result contains the raw signin link. + */ + CompletableFuture getOAuthSignInLink(TurnContext turnContext, String connectionName); + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated with the token. + * @param finalRedirect The final URL that the OAuth flow will redirect to. + * @return A task that represents the work queued to execute. If the task + * completes successfully, the result contains the raw signin link. + */ + CompletableFuture getOAuthSignInLink( + TurnContext turnContext, + String connectionName, + String userId, + String finalRedirect + ); + + /** + * Signs the user out with the token server. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @return A task that represents the work queued to execute. + */ + default CompletableFuture signOutUser(TurnContext turnContext) { + return signOutUser(turnContext, null, null); + } + + /** + * Signs the user out with the token server. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName Name of the auth connection to use. + * @param userId User id of user to sign out. + * @return A task that represents the work queued to execute. + */ + CompletableFuture signOutUser( + TurnContext turnContext, + String connectionName, + String userId + ); + + /** + * Retrieves the token status for each configured connection for the given user. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param userId The user Id for which token status is retrieved. + * @return Array of TokenStatus. + */ + default CompletableFuture> getTokenStatus( + TurnContext turnContext, + String userId + ) { + return getTokenStatus(turnContext, userId, null); + } + + /** + * Retrieves the token status for each configured connection for the given user. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param userId The user Id for which token status is retrieved. + * @param includeFilter Comma separated list of connection's to include. Blank + * will return token status for all configured connections. + * @return Array of TokenStatus. + */ + CompletableFuture> getTokenStatus( + TurnContext turnContext, + String userId, + String includeFilter + ); + + /** + * Retrieves Azure Active Directory tokens for particular resources on a + * configured connection. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName The name of the Azure Active Directory connection + * configured with this bot. + * @param resourceUrls The list of resource URLs to retrieve tokens for. + * @return Dictionary of resourceUrl to the corresponding TokenResponse. + */ + default CompletableFuture> getAadTokens( + TurnContext turnContext, + String connectionName, + String[] resourceUrls + ) { + return getAadTokens(turnContext, connectionName, resourceUrls, null); + } + + /** + * Retrieves Azure Active Directory tokens for particular resources on a + * configured connection. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param connectionName The name of the Azure Active Directory connection + * configured with this bot. + * @param resourceUrls The list of resource URLs to retrieve tokens for. + * @param userId The user Id for which tokens are retrieved. If passing + * in null the userId is taken from the Activity in the + * ITurnContext. + * @return Dictionary of resourceUrl to the corresponding TokenResponse. + */ + CompletableFuture> getAadTokens( + TurnContext turnContext, + String connectionName, + String[] resourceUrls, + String userId + ); + + + /** + * Attempts to retrieve the token for a user that's in a login flow, using + * customized AppCredentials. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param magicCode (Optional) Optional user entered code + * to validate. + * + * @return Token Response. + */ + CompletableFuture getUserToken( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName, + String magicCode); + + /** + * Get the raw signin link to be sent to the user for signin for a + * connection name, using customized AppCredentials. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * + * @return A CompletableFuture that represents the work queued to execute. + * + * If the CompletableFuture completes successfully, the result contains the raw signin + * link. + */ + CompletableFuture getOAuthSignInLink( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName); + + /** + * Get the raw signin link to be sent to the user for signin for a + * connection name, using customized AppCredentials. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated + * with the token. + * @param finalRedirect The final URL that the OAuth flow + * will redirect to. + * + * @return A CompletableFuture that represents the work queued to execute. + * + * If the CompletableFuture completes successfully, the result contains the raw signin + * link. + */ + CompletableFuture getOAuthSignInLink( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId, + String finalRedirect); + + /** + * Signs the user out with the token server, using customized + * AppCredentials. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId User id of user to sign out. + * + * @return A CompletableFuture that represents the work queued to execute. + */ + CompletableFuture signOutUser( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId); + + /** + * Retrieves the token status for each configured connection for the given + * user, using customized AppCredentials. + * + * @param context Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param userId The user Id for which token status is + * retrieved. + * @param includeFilter Optional comma separated list of + * connection's to include. Blank will return token status for all + * configured connections. + * + * @return Array of TokenStatus. + */ + CompletableFuture> getTokenStatus( + TurnContext context, + AppCredentials oAuthAppCredentials, + String userId, + String includeFilter); + + /** + * Retrieves Azure Active Directory tokens for particular resources on a + * configured connection, using customized AppCredentials. + * + * @param context Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName The name of the Azure Active + * Directory connection configured with this bot. + * @param resourceUrls The list of resource URLs to retrieve + * tokens for. + * @param userId The user Id for which tokens are + * retrieved. If passing in null the userId is taken from the Activity in + * the TurnContext. + * + * @return Dictionary of resourceUrl to the corresponding + * TokenResponse. + */ + CompletableFuture> getAadTokens( + TurnContext context, + AppCredentials oAuthAppCredentials, + String connectionName, + String[] resourceUrls, + String userId); + + /** + * Get the raw signin link to be sent to the user for signin for a + * connection name. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param connectionName Name of the auth connection to use. + * + * @return A CompletableFuture that represents the work queued to execute. + * + * If the CompletableFuture completes successfully, the result contains the raw signin + * link. + */ + CompletableFuture getSignInResource( + TurnContext turnContext, + String connectionName); + + /** + * Get the raw signin link to be sent to the user for signin for a + * connection name. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated with + * the token. + * @param finalRedirect The final URL that the OAuth flow will + * redirect to. + * + * @return A CompletableFuture that represents the work queued to execute. + * + * If the CompletableFuture completes successfully, the result contains the raw signin + * link. + */ + CompletableFuture getSignInResource( + TurnContext turnContext, + String connectionName, + String userId, + String finalRedirect); + + /** + * Get the raw signin link to be sent to the user for signin for a + * connection name. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials Credentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId The user id that will be associated + * with the token. + * @param finalRedirect The final URL that the OAuth flow + * will redirect to. + * + * @return A CompletableFuture that represents the work queued to execute. + * + * If the CompletableFuture completes successfully, the result contains the raw signin + * link. + */ + CompletableFuture getSignInResource( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId, + String finalRedirect); + + /** + * Performs a token exchange operation such as for single sign-on. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param connectionName Name of the auth connection to use. + * @param userId The user id associated with the token.. + * @param exchangeRequest The exchange request details, either a + * token to exchange or a uri to exchange. + * + * @return If the CompletableFuture completes, the exchanged token is returned. + */ + CompletableFuture exchangeToken( + TurnContext turnContext, + String connectionName, + String userId, + TokenExchangeRequest exchangeRequest); + + /** + * Performs a token exchange operation such as for single sign-on. + * + * @param turnContext Context for the current turn of + * conversation with the user. + * @param oAuthAppCredentials AppCredentials for OAuth. + * @param connectionName Name of the auth connection to use. + * @param userId The user id associated with the + * token.. + * @param exchangeRequest The exchange request details, either + * a token to exchange or a uri to exchange. + * + * @return If the CompletableFuture completes, the exchanged token is returned. + */ + CompletableFuture exchangeToken( + TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String connectionName, + String userId, + TokenExchangeRequest exchangeRequest); + +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionActivityExtensions.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionActivityExtensions.java new file mode 100644 index 000000000..c6c517dd6 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionActivityExtensions.java @@ -0,0 +1,63 @@ +// CHECKSTYLE:OFF +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.inspection; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; + +/** + * Helper class for the inspection middleware. + */ +final class InspectionActivityExtensions { + private InspectionActivityExtensions() { + + } + + static Activity makeCommandActivity(String command) { + return Activity.createTraceActivity( + "Command", + "https://www.botframework.com/schemas/command", + command, + "Command" + ); + } + + static Activity traceActivity(JsonNode state) { + return Activity.createTraceActivity( + "BotState", + "https://www.botframework.com/schemas/botState", + state, + "Bot State" + ); + } + + static Activity traceActivity(Activity activity, String name, String label) { + return Activity.createTraceActivity( + name, + "https://www.botframework.com/schemas/activity", + activity, + label + ); + } + + static Activity traceActivity(ConversationReference conversationReference) { + return Activity.createTraceActivity( + "MessageDelete", + "https://www.botframework.com/schemas/conversationReference", + conversationReference, + "Deleted Message" + ); + } + + static Activity traceActivity(Throwable exception) { + return Activity.createTraceActivity( + "TurnError", + "https://www.botframework.com/schemas/error", + exception.getMessage(), + "Turn Error" + ); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java new file mode 100644 index 000000000..eb747536f --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java @@ -0,0 +1,284 @@ +// CHECKSTYLE:OFF +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.inspection; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationReference; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class InspectionMiddleware extends InterceptionMiddleware { + private static final String COMMAND = "/INSPECT"; + + private InspectionState inspectionState; + private UserState userState; + private ConversationState conversationState; + private MicrosoftAppCredentials credentials; + + public InspectionMiddleware(InspectionState withInspectionState) { + this(withInspectionState, null, null, null); + } + + public InspectionMiddleware( + InspectionState withInspectionState, + UserState withUserState, + ConversationState withConversationState, + MicrosoftAppCredentials withCredentials + ) { + super(LoggerFactory.getLogger(InspectionMiddleware.class)); + + inspectionState = withInspectionState; + userState = withUserState; + conversationState = withConversationState; + credentials = withCredentials != null ? withCredentials : MicrosoftAppCredentials.empty(); + } + + public CompletableFuture processCommand(TurnContext turnContext) { + if ( + !turnContext.getActivity().isType(ActivityTypes.MESSAGE) + || StringUtils.isEmpty(turnContext.getActivity().getText()) + ) { + + return CompletableFuture.completedFuture(false); + } + + String text = Activity.removeRecipientMentionImmutable(turnContext.getActivity()); + String[] command = text.split(" "); + + if (command.length > 1 && StringUtils.equals(command[0], COMMAND)) { + if (command.length == 2 && StringUtils.equals(command[1], "open")) { + return processOpenCommand(turnContext).thenApply((result) -> true); + } + + if (command.length == 3 && StringUtils.equals(command[1], "attach")) { + return processAttachCommand(turnContext, command[2]).thenApply((result) -> true); + } + } + + return CompletableFuture.completedFuture(false); + } + + @Override + protected CompletableFuture inbound(TurnContext turnContext, Activity activity) { + return processCommand(turnContext).thenCompose(processResult -> { + if (processResult) { + return CompletableFuture.completedFuture(new Intercept(false, false)); + } + + return findSession(turnContext).thenCompose(session -> { + if (session == null) { + return CompletableFuture.completedFuture(new Intercept(true, false)); + } + + return invokeSend(turnContext, session, activity).thenCompose(invokeResult -> { + if (invokeResult) { + return CompletableFuture.completedFuture(new Intercept(true, true)); + } + return CompletableFuture.completedFuture(new Intercept(true, false)); + }); + }); + }); + } + + @Override + protected CompletableFuture outbound( + TurnContext turnContext, + List clonedActivities + ) { + return findSession(turnContext).thenCompose(session -> { + if (session != null) { + List> sends = new ArrayList<>(); + + for (Activity traceActivity : clonedActivities) { + sends.add(invokeSend(turnContext, session, traceActivity)); + } + + return CompletableFuture.allOf(sends.toArray(new CompletableFuture[sends.size()])); + } + + return CompletableFuture.completedFuture(null); + }); + } + + @Override + protected CompletableFuture traceState(TurnContext turnContext) { + return findSession(turnContext).thenCompose(session -> { + if (session == null) { + return CompletableFuture.completedFuture(null); + } + + CompletableFuture userLoad = userState == null + ? CompletableFuture.completedFuture(null) + : userState.load(turnContext); + + CompletableFuture conversationLoad = conversationState == null + ? CompletableFuture.completedFuture(null) + : conversationState.load(turnContext); + + return CompletableFuture.allOf(userLoad, conversationLoad).thenCompose(loadResult -> { + ObjectNode botState = JsonNodeFactory.instance.objectNode(); + if (userState != null) { + botState.set("userState", userState.get(turnContext)); + } + + if (conversationState != null) { + botState.set("conversationState", conversationState.get(turnContext)); + } + + return invokeSend( + turnContext, + session, + InspectionActivityExtensions.traceActivity(botState) + ).thenCompose(invokeResult -> CompletableFuture.completedFuture(null)); + }); + }); + } + + private CompletableFuture processOpenCommand(TurnContext turnContext) { + StatePropertyAccessor accessor = inspectionState.createProperty( + InspectionSessionsByStatus.class.getName() + ); + + return accessor.get(turnContext, InspectionSessionsByStatus::new).thenCompose(result -> { + InspectionSessionsByStatus sessions = (InspectionSessionsByStatus) result; + String sessionId = openCommand( + sessions, + turnContext.getActivity().getConversationReference() + ); + + String command = String.format("%s attach %s", COMMAND, sessionId); + return turnContext.sendActivity( + InspectionActivityExtensions.makeCommandActivity(command) + ); + }).thenCompose(resourceResponse -> inspectionState.saveChanges(turnContext)); + } + + private CompletableFuture processAttachCommand( + TurnContext turnContext, + String sessionId + ) { + StatePropertyAccessor accessor = inspectionState.createProperty( + InspectionSessionsByStatus.class.getName() + ); + + return accessor.get(turnContext, InspectionSessionsByStatus::new).thenCompose(sessions -> { + if ( + attachCommand( + turnContext.getActivity().getConversation().getId(), + sessions, + sessionId + ) + ) { + return turnContext.sendActivity( + MessageFactory.text( + "Attached to session, all traffic is being replicated for inspection." + ) + ); + } else { + return turnContext.sendActivity( + MessageFactory.text( + String.format("Open session with id %s does not exist.", sessionId) + ) + ); + } + }).thenCompose(resourceResponse -> inspectionState.saveChanges(turnContext)); + } + + private String openCommand( + InspectionSessionsByStatus sessions, + ConversationReference conversationReference + ) { + String sessionId = UUID.randomUUID().toString(); + sessions.getOpenedSessions().put(sessionId, conversationReference); + return sessionId; + } + + private boolean attachCommand( + String conversationId, + InspectionSessionsByStatus sessions, + String sessionId + ) { + ConversationReference inspectionSessionState = sessions.getOpenedSessions().get(sessionId); + if (inspectionSessionState == null) + return false; + + sessions.getAttachedSessions().put(conversationId, inspectionSessionState); + sessions.getOpenedSessions().remove(sessionId); + + return true; + } + + protected InspectionSession createSession( + ConversationReference reference, + MicrosoftAppCredentials credentials + ) { + return new InspectionSession(reference, credentials); + } + + private CompletableFuture findSession(TurnContext turnContext) { + StatePropertyAccessor accessor = inspectionState.createProperty( + InspectionSessionsByStatus.class.getName() + ); + + return accessor.get(turnContext, InspectionSessionsByStatus::new).thenApply( + openSessions -> { + ConversationReference reference = openSessions.getAttachedSessions().get( + turnContext.getActivity().getConversation().getId() + ); + + if (reference != null) { + return createSession(reference, credentials); + } + + return null; + } + ); + } + + private CompletableFuture invokeSend( + TurnContext turnContext, + InspectionSession session, + Activity activity + ) { + + return session.send(activity).thenCompose(result -> { + if (result) { + return CompletableFuture.completedFuture(true); + } + + return cleanupSession(turnContext).thenCompose( + cleanupResult -> CompletableFuture.completedFuture(false) + ); + }); + } + + private CompletableFuture cleanupSession(TurnContext turnContext) { + StatePropertyAccessor accessor = inspectionState.createProperty( + InspectionSessionsByStatus.class.getName() + ); + + return accessor.get(turnContext, InspectionSessionsByStatus::new).thenCompose(result -> { + InspectionSessionsByStatus openSessions = (InspectionSessionsByStatus) result; + openSessions.getAttachedSessions().remove( + turnContext.getActivity().getConversation().getId() + ); + return inspectionState.saveChanges(turnContext); + }); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionSession.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionSession.java new file mode 100644 index 000000000..3dcf1141f --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionSession.java @@ -0,0 +1,61 @@ +// CHECKSTYLE:OFF +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.inspection; + +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +public class InspectionSession { + private ConversationReference conversationReference; + private Logger logger; + private ConnectorClient connectorClient; + + public InspectionSession( + ConversationReference withConversationReference, + MicrosoftAppCredentials withCredentials + ) { + this(withConversationReference, withCredentials, null); + } + + public InspectionSession( + ConversationReference withConversationReference, + MicrosoftAppCredentials withCredentials, + Logger withLogger + ) { + conversationReference = withConversationReference; + logger = withLogger != null ? withLogger : LoggerFactory.getLogger(InspectionSession.class); + connectorClient = new RestConnectorClient( + conversationReference.getServiceUrl(), + withCredentials + ); + } + + public CompletableFuture send(Activity activity) { + return connectorClient.getConversations().sendToConversation( + activity.applyConversationReference(conversationReference) + ) + + .handle((result, exception) -> { + if (exception == null) { + return true; + } + + logger.warn( + "Exception '{}' while attempting to call Emulator for inspection, check it is running, " + + "and you have correct credentials in the Emulator and the InspectionMiddleware.", + exception.getMessage() + ); + + return false; + }); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionSessionsByStatus.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionSessionsByStatus.java new file mode 100644 index 000000000..2f5837547 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionSessionsByStatus.java @@ -0,0 +1,31 @@ +// CHECKSTYLE:OFF +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.inspection; + +import com.microsoft.bot.schema.ConversationReference; + +import java.util.HashMap; +import java.util.Map; + +public class InspectionSessionsByStatus { + private Map openedSessions = new HashMap<>(); + private Map attachedSessions = new HashMap<>(); + + public Map getAttachedSessions() { + return attachedSessions; + } + + public void setAttachedSessions(Map attachedSessions) { + this.attachedSessions = attachedSessions; + } + + public Map getOpenedSessions() { + return openedSessions; + } + + public void setOpenedSessions(Map openedSessions) { + this.openedSessions = openedSessions; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionState.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionState.java new file mode 100644 index 000000000..2291eab7c --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionState.java @@ -0,0 +1,25 @@ +// CHECKSTYLE:OFF +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.inspection; + +import com.microsoft.bot.builder.BotState; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.TurnContext; + +public class InspectionState extends BotState { + /** + * Initializes a new instance of the BotState class. + * + * @param withStorage The storage provider to use. + */ + public InspectionState(Storage withStorage) { + super(withStorage, InspectionState.class.getSimpleName()); + } + + @Override + public String getStorageKey(TurnContext turnContext) { + return InspectionState.class.getSimpleName(); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java new file mode 100644 index 000000000..7b6f7ac29 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java @@ -0,0 +1,166 @@ +// CHECKSTYLE:OFF +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.inspection; + +import com.microsoft.bot.builder.Middleware; +import com.microsoft.bot.builder.NextDelegate; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.schema.Activity; +import org.slf4j.Logger; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; + +public abstract class InterceptionMiddleware implements Middleware { + private Logger logger; + + static class Intercept { + Intercept( + boolean forward, + boolean intercept + ) { + shouldForwardToApplication = forward; + shouldIntercept = intercept; + } + + @SuppressWarnings({ "checkstyle:JavadocVariable", "checkstyle:VisibilityModifier" }) + boolean shouldForwardToApplication; + @SuppressWarnings({ "checkstyle:JavadocVariable", "checkstyle:VisibilityModifier" }) + boolean shouldIntercept; + } + + public InterceptionMiddleware(Logger withLogger) { + logger = withLogger; + } + + protected Logger getLogger() { + return logger; + } + + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + return invokeInbound( + turnContext, + InspectionActivityExtensions.traceActivity( + turnContext.getActivity(), + "ReceivedActivity", + "Received Activity" + ) + ) + + .thenCompose(intercept -> { + if (intercept.shouldIntercept) { + turnContext.onSendActivities((sendContext, sendActivities, sendNext) -> { + List traceActivities = sendActivities.stream().map( + a -> InspectionActivityExtensions.traceActivity( + a, + "SentActivity", + "Sent Activity" + ) + ).collect(Collectors.toList()); + return invokeOutbound(sendContext, traceActivities).thenCompose( + response -> { + return sendNext.get(); + } + ); + }); + + turnContext.onUpdateActivity((updateContext, updateActivity, updateNext) -> { + Activity traceActivity = InspectionActivityExtensions.traceActivity( + updateActivity, + "MessageUpdate", + "Message Update" + ); + return invokeOutbound(turnContext, traceActivity).thenCompose( + response -> updateNext.get() + ); + }); + + turnContext.onDeleteActivity((deleteContext, deleteReference, deleteNext) -> { + Activity traceActivity = InspectionActivityExtensions.traceActivity( + deleteReference + ); + return invokeOutbound(turnContext, traceActivity).thenCompose( + response -> deleteNext.get() + ); + }); + } + + if (intercept.shouldForwardToApplication) { + next.next().exceptionally(exception -> { + Activity traceActivity = InspectionActivityExtensions.traceActivity( + exception + ); + invokeTraceException(turnContext, traceActivity).join(); + throw new CompletionException(exception); + }).join(); + } + + if (intercept.shouldIntercept) { + return invokeTraceState(turnContext); + } + + return CompletableFuture.completedFuture(null); + }); + } + + protected abstract CompletableFuture inbound( + TurnContext turnContext, + Activity activity + ); + + protected abstract CompletableFuture outbound( + TurnContext turnContext, + List clonedActivities + ); + + protected abstract CompletableFuture traceState(TurnContext turnContext); + + private CompletableFuture invokeInbound( + TurnContext turnContext, + Activity traceActivity + ) { + return inbound(turnContext, traceActivity).exceptionally(exception -> { + logger.warn("Exception in inbound interception {}", exception.getMessage()); + return new Intercept(true, false); + }); + } + + private CompletableFuture invokeOutbound( + TurnContext turnContext, + List traceActivities + ) { + return outbound(turnContext, traceActivities).exceptionally(exception -> { + logger.warn("Exception in outbound interception {}", exception.getMessage()); + return null; + }); + } + + private CompletableFuture invokeOutbound(TurnContext turnContext, Activity activity) { + return invokeOutbound(turnContext, Collections.singletonList(activity)); + } + + private CompletableFuture invokeTraceState(TurnContext turnContext) { + return traceState(turnContext).exceptionally(exception -> { + logger.warn("Exception in state interception {}", exception.getMessage()); + return null; + }); + } + + private CompletableFuture invokeTraceException( + TurnContext turnContext, + Activity traceActivity + ) { + return outbound(turnContext, Collections.singletonList(traceActivity)).exceptionally( + exception -> { + logger.warn("Exception in exception interception {}", exception.getMessage()); + return null; + } + ); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/package-info.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/package-info.java new file mode 100644 index 000000000..84bce0bc9 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.builder.inspection. + */ +package com.microsoft.bot.builder.inspection; diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/integration/AdapterIntegration.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/integration/AdapterIntegration.java new file mode 100644 index 000000000..dde2c2a9d --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/integration/AdapterIntegration.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.integration; + +import com.microsoft.bot.builder.BotCallbackHandler; +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; + +import java.util.concurrent.CompletableFuture; + +/** + * An interface that defines the contract between web service integration pieces + * and the bot adapter. + */ +public interface AdapterIntegration { + /** + * Creates a turn context and runs the middleware pipeline for an incoming + * activity. + * + * @param authHeader The HTTP authentication header of the request. + * @param activity The incoming activity. + * @param callback The code to run at the end of the adapter's middleware + * pipeline. + * @return A task that represents the work queued to execute. If the activity + * type was 'Invoke' and the corresponding key (channelId + activityId) + * was found then an InvokeResponse is returned, otherwise null is + * returned. + */ + CompletableFuture processActivity( + String authHeader, + Activity activity, + BotCallbackHandler callback + ); + + /** + * Sends a proactive message to a conversation. + * + *

+ * Call this method to proactively send a message to a conversation. Most + * _channels require a user to initiate a conversation with a bot before the bot + * can send activities to the user. + *

+ * + * @param botId The application ID of the bot. This parameter is ignored in + * single tenant the Adapters (Console, Test, etc) but is + * critical to the BotFrameworkAdapter which is multi-tenant + * aware. + * @param reference A reference to the conversation to continue. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + */ + CompletableFuture continueConversation( + String botId, + ConversationReference reference, + BotCallbackHandler callback + ); +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/integration/package-info.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/integration/package-info.java new file mode 100644 index 000000000..379395d3e --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/integration/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.builder.integration. + */ +package com.microsoft.bot.builder.integration; diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/package-info.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/package-info.java new file mode 100644 index 000000000..dfc9bd89e --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.builder. + */ +@Deprecated +package com.microsoft.bot.builder; diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java new file mode 100644 index 000000000..0db9994bd --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TypedInvokeResponse; +import com.microsoft.bot.schema.Activity; + +/** + * A Bot Framework client. + */ +public abstract class BotFrameworkClient { + + // /** + // * Forwards an activity to a skill (bot). + // * + // * NOTE: Forwarding an activity to a skill will flush UserState and + // * ConversationState changes so that skill has accurate state. + // * + // * @param fromBotId The MicrosoftAppId of the bot sending the + // * activity. + // * @param toBotId The MicrosoftAppId of the bot receiving + // * the activity. + // * @param toUrl The URL of the bot receiving the activity. + // * @param serviceUrl The callback Url for the skill host. + // * @param conversationId A conversation ID to use for the + // * conversation with the skill. + // * @param activity The {@link Activity} to send to forward. + // * + // * @return task with optional invokeResponse. + // */ + // public abstract CompletableFuture postActivity( + // String fromBotId, + // String toBotId, + // URI toUrl, + // URI serviceUrl, + // String conversationId, + // Activity activity); + + /** + * Forwards an activity to a skill (bot). + * + * NOTE: Forwarding an activity to a skill will flush UserState and + * ConversationState changes so that skill has accurate state. + * + * @param fromBotId The MicrosoftAppId of the bot sending the + * activity. + * @param toBotId The MicrosoftAppId of the bot receiving + * the activity. + * @param toUri The URL of the bot receiving the activity. + * @param serviceUri The callback Url for the skill host. + * @param conversationId A conversation ID to use for the + * conversation with the skill. + * @param activity The {@link Activity} to send to forward. + * @param type The type for the response body to contain, can't really use due to type erasure + * in Java. + * @param The type for the TypedInvokeResponse body to contain. + * + * @return task with optional invokeResponse. + */ + public abstract CompletableFuture> postActivity( + String fromBotId, + String toBotId, + URI toUri, + URI serviceUri, + String conversationId, + Activity activity, + Class type); +} + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkSkill.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkSkill.java new file mode 100644 index 000000000..99f4f97f1 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkSkill.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import java.net.URI; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Registration for a BotFrameworkHttpProtocol super. Skill endpoint. + */ +public class BotFrameworkSkill { + + @JsonProperty(value = "id") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String id; + + @JsonProperty(value = "appId") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String appId; + + @JsonProperty(value = "skillEndpoint") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private URI skillEndpoint; + + /** + * Gets Id of the skill. + * @return the Id value as a String. + */ + public String getId() { + return this.id; + } + + /** + * Sets Id of the skill. + * @param withId The Id value. + */ + public void setId(String withId) { + this.id = withId; + } + /** + * Gets appId of the skill. + * @return the AppId value as a String. + */ + public String getAppId() { + return this.appId; + } + + /** + * Sets appId of the skill. + * @param withAppId The AppId value. + */ + public void setAppId(String withAppId) { + this.appId = withAppId; + } + /** + * Gets /api/messages endpoint for the skill. + * @return the SkillEndpoint value as a Uri. + */ + public URI getSkillEndpoint() { + return this.skillEndpoint; + } + + /** + * Sets /api/messages endpoint for the skill. + * @param withSkillEndpoint The SkillEndpoint value. + */ + public void setSkillEndpoint(URI withSkillEndpoint) { + this.skillEndpoint = withSkillEndpoint; + } +} + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactory.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactory.java new file mode 100644 index 000000000..a38214d25 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactory.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder.skills; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.ConversationReference; + +import org.apache.commons.lang3.StringUtils; + +/** + * A {@link SkillConversationIdFactory} that uses an in memory + * {@link Map{TKey,TValue}} to store and retrieve {@link ConversationReference} + * instances. + */ +public class SkillConversationIdFactory extends SkillConversationIdFactoryBase { + + private Storage storage; + + /** + * Creates an instance of a SkillConversationIdFactory. + * + * @param storage A storage instance for the factory. + */ + public SkillConversationIdFactory(Storage storage) { + if (storage == null) { + throw new IllegalArgumentException("Storage cannot be null."); + } + this.storage = storage; + } + + /** + * Creates a conversation id for a skill conversation. + * + * @param options A {@link SkillConversationIdFactoryOptions} instance + * containing parameters for creating the conversation ID. + * + * @return A unique conversation ID used to communicate with the skill. + * + * It should be possible to use the returned String on a request URL and + * it should not contain special characters. + */ + @Override + public CompletableFuture createSkillConversationId(SkillConversationIdFactoryOptions options) { + if (options == null) { + Async.completeExceptionally(new IllegalArgumentException("options cannot be null.")); + } + ConversationReference conversationReference = options.getActivity().getConversationReference(); + String skillConversationId = UUID.randomUUID().toString(); + + SkillConversationReference skillConversationReference = new SkillConversationReference(); + skillConversationReference.setConversationReference(conversationReference); + skillConversationReference.setOAuthScope(options.getFromBotOAuthScope()); + Map skillConversationInfo = new HashMap(); + skillConversationInfo.put(skillConversationId, skillConversationReference); + return storage.write(skillConversationInfo) + .thenCompose(result -> CompletableFuture.completedFuture(skillConversationId)); + } + + /** + * Gets the {@link SkillConversationReference} created using + * {@link SkillConversationIdFactory#createSkillConversationId} for a + * skillConversationId. + * + * @param skillConversationId A skill conversationId created using + * {@link SkillConversationIdFactory#createSkillConversationId}. + * + * @return The caller's {@link ConversationReference} for a skillConversationId. + * null if not found. + */ + @Override + public CompletableFuture getSkillConversationReference(String skillConversationId) { + if (StringUtils.isAllBlank(skillConversationId)) { + Async.completeExceptionally(new IllegalArgumentException("skillConversationId cannot be null.")); + } + + return storage.read(new String[] {skillConversationId}).thenCompose(skillConversationInfo -> { + if (skillConversationInfo.size() > 0) { + return CompletableFuture + .completedFuture((SkillConversationReference) skillConversationInfo.get(skillConversationId)); + } else { + return CompletableFuture.completedFuture(null); + } + }); + } + + /** + * Deletes a {@link ConversationReference} . + * + * @param skillConversationId A skill conversationId created using {@link + * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT + * reading()#getCancellationToken())} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture deleteConversationReference(String skillConversationId) { + return storage.delete(new String[] {skillConversationId}); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryBase.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryBase.java new file mode 100644 index 000000000..8e912ba73 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryBase.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.schema.ConversationReference; + +import org.apache.commons.lang3.NotImplementedException; + +/** + * Defines the interface of a factory that is used to create unique conversation + * IDs for skill conversations. + */ +public abstract class SkillConversationIdFactoryBase { + + /** + * Creates a conversation ID for a skill conversation super. on the + * caller's {@link ConversationReference} . + * + * @param conversationReference The skill's caller {@link ConversationReference} . + * + * @return A unique conversation ID used to communicate with the + * skill. + * + * It should be possible to use the returned String on a request URL and it + * should not contain special characters. + */ + public CompletableFuture createSkillConversationId(ConversationReference conversationReference) { + throw new NotImplementedException("createSkillConversationId"); + } + + /** + * Creates a conversation id for a skill conversation. + * + * @param options A {@link SkillConversationIdFactoryOptions} + * instance containing parameters for creating the conversation ID. + * + * @return A unique conversation ID used to communicate with the skill. + * + * It should be possible to use the returned String on a request URL and it + * should not contain special characters. + */ + public CompletableFuture createSkillConversationId(SkillConversationIdFactoryOptions options) { + throw new NotImplementedException("createSkillConversationId"); + } + + /** + * Gets the {@link ConversationReference} created using + * {@link + * CreateSkillConversationId(Microsoft#getBot()#getSchema()#getConversatio + * Reference(),System#getThreading()#getCancellationToken())} for a + * skillConversationId. + * + * @param skillConversationId A skill conversationId created using {@link + * CreateSkillConversationId(Microsoft#getBot()#getSchema()#getConversatio + * Reference(),System#getThreading()#getCancellationToken())} . + * + * @return The caller's {@link ConversationReference} for a skillConversationId. null if not found. + */ + public CompletableFuture getConversationReference(String skillConversationId) { + throw new NotImplementedException("getConversationReference"); + } + + /** + * Gets the {@link SkillConversationReference} used during {@link + * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT + * reading()#getCancellationToken())} for a skillConversationId. + * + * @param skillConversationId A skill conversationId created using {@link + * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT + * reading()#getCancellationToken())} . + * + * @return The caller's {@link ConversationReference} for a skillConversationId, with originatingAudience. + * Null if not found. + */ + public CompletableFuture getSkillConversationReference(String skillConversationId) { + throw new NotImplementedException("getSkillConversationReference"); + } + + /** + * Deletes a {@link ConversationReference} . + * + * @param skillConversationId A skill conversationId created using {@link + * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT + * reading()#getCancellationToken())} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + public abstract CompletableFuture deleteConversationReference(String skillConversationId); +} + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryOptions.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryOptions.java new file mode 100644 index 000000000..c0ffde7ef --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryOptions.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import com.microsoft.bot.schema.Activity; + +/** + * A class defining the parameters used in + * {@link SkillConversationIdFactoryBase#createSkillConversationId(SkillConversationI + * FactoryOptions,System#getThreading()#getCancellationToken())} . + */ +public class SkillConversationIdFactoryOptions { + + private String fromBotOAuthScope; + + private String fromBotId; + + private Activity activity; + + private BotFrameworkSkill botFrameworkSkill; + + /** + * Gets the oauth audience scope, used during token retrieval + * (either https://api.getbotframework().com or bot app id). + * @return the FromBotOAuthScope value as a String. + */ + public String getFromBotOAuthScope() { + return this.fromBotOAuthScope; + } + + /** + * Sets the oauth audience scope, used during token retrieval + * (either https://api.getbotframework().com or bot app id). + * @param withFromBotOAuthScope The FromBotOAuthScope value. + */ + public void setFromBotOAuthScope(String withFromBotOAuthScope) { + this.fromBotOAuthScope = withFromBotOAuthScope; + } + + /** + * Gets the id of the parent bot that is messaging the skill. + * @return the FromBotId value as a String. + */ + public String getFromBotId() { + return this.fromBotId; + } + + /** + * Sets the id of the parent bot that is messaging the skill. + * @param withFromBotId The FromBotId value. + */ + public void setFromBotId(String withFromBotId) { + this.fromBotId = withFromBotId; + } + + /** + * Gets the activity which will be sent to the skill. + * @return the Activity value as a getActivity(). + */ + public Activity getActivity() { + return this.activity; + } + + /** + * Sets the activity which will be sent to the skill. + * @param withActivity The Activity value. + */ + public void setActivity(Activity withActivity) { + this.activity = withActivity; + } + /** + * Gets the skill to create the conversation Id for. + * @return the BotFrameworkSkill value as a getBotFrameworkSkill(). + */ + public BotFrameworkSkill getBotFrameworkSkill() { + return this.botFrameworkSkill; + } + + /** + * Sets the skill to create the conversation Id for. + * @param withBotFrameworkSkill The BotFrameworkSkill value. + */ + public void setBotFrameworkSkill(BotFrameworkSkill withBotFrameworkSkill) { + this.botFrameworkSkill = withBotFrameworkSkill; + } +} + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationReference.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationReference.java new file mode 100644 index 000000000..5e538fbe7 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationReference.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import com.microsoft.bot.schema.ConversationReference; + +/** + * A conversation reference type for skills. + */ +public class SkillConversationReference { + + private ConversationReference conversationReference; + + private String oAuthScope; + + /** + * Gets the conversation reference. + * @return the ConversationReference value as a getConversationReference(). + */ + public ConversationReference getConversationReference() { + return this.conversationReference; + } + + /** + * Sets the conversation reference. + * @param withConversationReference The ConversationReference value. + */ + public void setConversationReference(ConversationReference withConversationReference) { + this.conversationReference = withConversationReference; + } + + /** + * Gets the OAuth scope. + * @return the OAuthScope value as a String. + */ + public String getOAuthScope() { + return this.oAuthScope; + } + + /** + * Sets the OAuth scope. + * @param withOAuthScope The OAuthScope value. + */ + public void setOAuthScope(String withOAuthScope) { + this.oAuthScope = withOAuthScope; + } + +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java new file mode 100644 index 000000000..e2107182f --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotCallbackHandler; +import com.microsoft.bot.builder.ChannelServiceHandler; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ChannelProvider; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.CredentialProvider; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.CallerIdConstants; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; + +import org.apache.commons.lang3.NotImplementedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Bot Framework Handler for skills. + */ +public class SkillHandler extends ChannelServiceHandler { + + /** + * The skill conversation reference. + */ + public static final String SKILL_CONVERSATION_REFERENCE_KEY = + "com.microsoft.bot.builder.skills.SkillConversationReference"; + + private final BotAdapter adapter; + private final Bot bot; + private final SkillConversationIdFactoryBase conversationIdFactory; + + /** + * The slf4j Logger to use. Note that slf4j is configured by providing Log4j + * dependencies in the POM, and corresponding Log4j configuration in the + * 'resources' folder. + */ + private Logger logger = LoggerFactory.getLogger(SkillHandler.class); + + /** + * Initializes a new instance of the {@link SkillHandler} class, using a + * credential provider. + * + * @param adapter An instance of the {@link BotAdapter} that will handle the request. + * @param bot The {@link IBot} instance. + * @param conversationIdFactory A {@link SkillConversationIdFactoryBase} to unpack the conversation ID and + * map it to the calling bot. + * @param credentialProvider The credential provider. + * @param authConfig The authentication configuration. + * @param channelProvider The channel provider. + * + * Use a {@link MiddlewareSet} Object to add multiple middleware components + * in the constructor. Use the Use({@link Middleware} ) method to add + * additional middleware to the adapter after construction. + */ + public SkillHandler( + BotAdapter adapter, + Bot bot, + SkillConversationIdFactoryBase conversationIdFactory, + CredentialProvider credentialProvider, + AuthenticationConfiguration authConfig, + ChannelProvider channelProvider + ) { + + super(credentialProvider, authConfig, channelProvider); + + if (adapter == null) { + throw new IllegalArgumentException("adapter cannot be null"); + } + + if (bot == null) { + throw new IllegalArgumentException("bot cannot be null"); + } + + if (conversationIdFactory == null) { + throw new IllegalArgumentException("conversationIdFactory cannot be null"); + } + + this.adapter = adapter; + this.bot = bot; + this.conversationIdFactory = conversationIdFactory; + } + + /** + * SendToConversation() API for Skill. + * + * This method allows you to send an activity to the end of a conversation. + * This is slightly different from ReplyToActivity(). * + * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the + * channel. * ReplyToActivity(conversationId,ActivityId) - adds the + * activity as a reply to another activity, if the channel supports it. If + * the channel does not support nested replies, ReplyToActivity falls back + * to SendToConversation. Use ReplyToActivity when replying to a specific + * activity in the conversation. Use SendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param activity Activity to send. + * + * @return task for a resource response. + */ + @Override + protected CompletableFuture onSendToConversation( + ClaimsIdentity claimsIdentity, + String conversationId, + Activity activity) { + return processActivity(claimsIdentity, conversationId, null, activity); + } + + /** + * ReplyToActivity() API for Skill. + * + * This method allows you to reply to an activity. This is slightly + * different from SendToConversation(). * + * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the + * channel. * ReplyToActivity(conversationId,ActivityId) - adds the + * activity as a reply to another activity, if the channel supports it. If + * the channel does not support nested replies, ReplyToActivity falls back + * to SendToConversation. Use ReplyToActivity when replying to a specific + * activity in the conversation. Use SendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param activityId activityId the reply is to (OPTIONAL). + * @param activity Activity to send. + * + * @return task for a resource response. + */ + @Override + protected CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + return processActivity(claimsIdentity, conversationId, activityId, activity); + } + + /** + */ + @Override + protected CompletableFuture onDeleteActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + + SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); + + BotCallbackHandler callback = turnContext -> { + turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference); + return turnContext.deleteActivity(activityId); + }; + + return adapter.continueConversation(claimsIdentity, + skillConversationReference.getConversationReference(), + skillConversationReference.getOAuthScope(), + callback); + } + + /** + */ + @Override + protected CompletableFuture onUpdateActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); + + AtomicReference resourceResponse = new AtomicReference(); + + BotCallbackHandler callback = turnContext -> { + turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference); + activity.applyConversationReference(skillConversationReference.getConversationReference()); + turnContext.getActivity().setId(activityId); + String callerId = String.format("%s%s", + CallerIdConstants.BOT_TO_BOT_PREFIX, + JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())); + turnContext.getActivity().setCallerId(callerId); + resourceResponse.set(turnContext.updateActivity(activity).join()); + return CompletableFuture.completedFuture(null); + }; + + adapter.continueConversation(claimsIdentity, + skillConversationReference.getConversationReference(), + skillConversationReference.getOAuthScope(), + callback); + + if (resourceResponse.get() != null) { + return CompletableFuture.completedFuture(resourceResponse.get()); + } else { + return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString())); + } + } + + private static void applyEoCToTurnContextActivity(TurnContext turnContext, Activity endOfConversationActivity) { + // transform the turnContext.Activity to be the EndOfConversation. + turnContext.getActivity().setType(endOfConversationActivity.getType()); + turnContext.getActivity().setText(endOfConversationActivity.getText()); + turnContext.getActivity().setCode(endOfConversationActivity.getCode()); + + turnContext.getActivity().setReplyToId(endOfConversationActivity.getReplyToId()); + turnContext.getActivity().setValue(endOfConversationActivity.getValue()); + turnContext.getActivity().setEntities(endOfConversationActivity.getEntities()); + turnContext.getActivity().setLocale(endOfConversationActivity.getLocale()); + turnContext.getActivity().setLocalTimestamp(endOfConversationActivity.getLocalTimestamp()); + turnContext.getActivity().setTimestamp(endOfConversationActivity.getTimestamp()); + turnContext.getActivity().setChannelData(endOfConversationActivity.getChannelData()); + for (Map.Entry entry : endOfConversationActivity.getProperties().entrySet()) { + turnContext.getActivity().setProperties(entry.getKey(), entry.getValue()); + } + } + + private static void applyEventToTurnContextActivity(TurnContext turnContext, Activity eventActivity) { + // transform the turnContext.Activity to be the EventActivity. + turnContext.getActivity().setType(eventActivity.getType()); + turnContext.getActivity().setName(eventActivity.getName()); + turnContext.getActivity().setValue(eventActivity.getValue()); + turnContext.getActivity().setRelatesTo(eventActivity.getRelatesTo()); + + turnContext.getActivity().setReplyToId(eventActivity.getReplyToId()); + turnContext.getActivity().setValue(eventActivity.getValue()); + turnContext.getActivity().setEntities(eventActivity.getEntities()); + turnContext.getActivity().setLocale(eventActivity.getLocale()); + turnContext.getActivity().setLocalTimestamp(eventActivity.getLocalTimestamp()); + turnContext.getActivity().setTimestamp(eventActivity.getTimestamp()); + turnContext.getActivity().setChannelData(eventActivity.getChannelData()); + for (Map.Entry entry : eventActivity.getProperties().entrySet()) { + turnContext.getActivity().setProperties(entry.getKey(), entry.getValue()); + } + } + + private CompletableFuture getSkillConversationReference(String conversationId) { + + SkillConversationReference skillConversationReference; + try { + skillConversationReference = conversationIdFactory.getSkillConversationReference(conversationId).join(); + } catch (NotImplementedException ex) { + if (logger != null) { + logger.warn("Got NotImplementedException when trying to call " + + "GetSkillConversationReference() on the ConversationIdFactory," + + " attempting to use deprecated GetConversationReference() method instead."); + } + + // Attempt to get SkillConversationReference using deprecated method. + // this catch should be removed once we remove the deprecated method. + // We need to use the deprecated method for backward compatibility. + ConversationReference conversationReference = + conversationIdFactory.getConversationReference(conversationId).join(); + skillConversationReference = new SkillConversationReference(); + skillConversationReference.setConversationReference(conversationReference); + if (getChannelProvider() != null && getChannelProvider().isGovernment()) { + skillConversationReference.setOAuthScope( + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + } else { + skillConversationReference.setOAuthScope( + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + } + } + + if (skillConversationReference == null) { + if (logger != null) { + logger.warn( + String.format("Unable to get skill conversation reference for conversationId %s.", conversationId) + ); + } + throw new RuntimeException("Key not found"); + } + + return CompletableFuture.completedFuture(skillConversationReference); + } + + private CompletableFuture processActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String replyToActivityId, + Activity activity) { + + SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); + + AtomicReference resourceResponse = new AtomicReference(); + + BotCallbackHandler callback = turnContext -> { + turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference); + activity.applyConversationReference(skillConversationReference.getConversationReference()); + turnContext.getActivity().setId(replyToActivityId); + String callerId = String.format("%s%s", + CallerIdConstants.BOT_TO_BOT_PREFIX, + JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())); + turnContext.getActivity().setCallerId(callerId); + + switch (activity.getType()) { + case ActivityTypes.END_OF_CONVERSATION: + conversationIdFactory.deleteConversationReference(conversationId).join(); + applyEoCToTurnContextActivity(turnContext, activity); + bot.onTurn(turnContext).join(); + break; + case ActivityTypes.EVENT: + applyEventToTurnContextActivity(turnContext, activity); + bot.onTurn(turnContext).join(); + break; + default: + resourceResponse.set(turnContext.sendActivity(activity).join()); + break; + } + return CompletableFuture.completedFuture(null); + }; + + adapter.continueConversation(claimsIdentity, + skillConversationReference.getConversationReference(), + skillConversationReference.getOAuthScope(), + callback).join(); + + if (resourceResponse.get() != null) { + return CompletableFuture.completedFuture(resourceResponse.get()); + } else { + return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString())); + } + } +} + diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/package-info.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/package-info.java new file mode 100644 index 000000000..5d86a65e1 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.builder.skills. + */ +package com.microsoft.bot.builder.skills; diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsActivityHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsActivityHandler.java new file mode 100644 index 000000000..5ef8317f1 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsActivityHandler.java @@ -0,0 +1,1060 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.rest.ErrorResponseException; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.Error; +import com.microsoft.bot.schema.ErrorResponse; +import com.microsoft.bot.schema.ResultPair; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.teams.AppBasedLinkQuery; +import com.microsoft.bot.schema.teams.ChannelInfo; +import com.microsoft.bot.schema.teams.FileConsentCardResponse; +import com.microsoft.bot.schema.teams.MeetingEndEventDetails; +import com.microsoft.bot.schema.teams.MeetingStartEventDetails; +import com.microsoft.bot.schema.teams.MessagingExtensionAction; +import com.microsoft.bot.schema.teams.MessagingExtensionActionResponse; +import com.microsoft.bot.schema.teams.MessagingExtensionQuery; +import com.microsoft.bot.schema.teams.MessagingExtensionResponse; +import com.microsoft.bot.schema.teams.O365ConnectorCardActionQuery; +import com.microsoft.bot.schema.teams.TabRequest; +import com.microsoft.bot.schema.teams.TabResponse; +import com.microsoft.bot.schema.teams.TabSubmit; +import com.microsoft.bot.schema.teams.TaskModuleRequest; +import com.microsoft.bot.schema.teams.TaskModuleResponse; +import com.microsoft.bot.schema.teams.TeamInfo; +import com.microsoft.bot.schema.teams.TeamsChannelAccount; +import com.microsoft.bot.schema.teams.TeamsChannelData; +import org.apache.commons.lang3.StringUtils; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A Teams implementation of the Bot interface intended for further subclassing. + * Derive from this class to plug in code to handle particular Activity types. + * Pre and post processing of Activities can be plugged in by deriving and + * calling the base class implementation. + */ +@SuppressWarnings({"checkstyle:JavadocMethod", "checkstyle:DesignForExtension", "checkstyle:MethodLength"}) +public class TeamsActivityHandler extends ActivityHandler { + /** + * Invoked when an invoke activity is received from the connector when the base + * behavior of onTurn is used. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + @Override + protected CompletableFuture onInvokeActivity(TurnContext turnContext) { + CompletableFuture result; + + try { + if (turnContext.getActivity().getName() == null && turnContext.getActivity().isTeamsActivity()) { + result = onTeamsCardActionInvoke(turnContext); + } else { + switch (turnContext.getActivity().getName()) { + case "fileConsent/invoke": + result = onTeamsFileConsent( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), FileConsentCardResponse.class) + ); + break; + + case "actionableMessage/executeAction": + result = onTeamsO365ConnectorCardAction( + turnContext, + Serialization + .safeGetAs(turnContext.getActivity().getValue(), O365ConnectorCardActionQuery.class) + ).thenApply(aVoid -> createInvokeResponse(null)); + break; + + case "composeExtension/queryLink": + result = onTeamsAppBasedLinkQuery( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), AppBasedLinkQuery.class) + ).thenApply(this::createInvokeResponse); + break; + + case "composeExtension/query": + result = onTeamsMessagingExtensionQuery( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), MessagingExtensionQuery.class) + ).thenApply(this::createInvokeResponse); + break; + + case "composeExtension/selectItem": + result = onTeamsMessagingExtensionSelectItem(turnContext, turnContext.getActivity().getValue()) + .thenApply(this::createInvokeResponse); + break; + + case "composeExtension/submitAction": + result = + onTeamsMessagingExtensionSubmitActionDispatch( + turnContext, + Serialization + .safeGetAs(turnContext.getActivity().getValue(), MessagingExtensionAction.class) + ).thenApply(this::createInvokeResponse); + break; + + case "composeExtension/fetchTask": + result = + onTeamsMessagingExtensionFetchTask( + turnContext, + Serialization + .safeGetAs(turnContext.getActivity().getValue(), MessagingExtensionAction.class) + ).thenApply(this::createInvokeResponse); + break; + + case "composeExtension/querySettingUrl": + result = onTeamsMessagingExtensionConfigurationQuerySettingUrl( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), MessagingExtensionQuery.class) + ).thenApply(this::createInvokeResponse); + break; + + case "composeExtension/setting": + result = onTeamsMessagingExtensionConfigurationSetting( + turnContext, + turnContext.getActivity().getValue() + ).thenApply(this::createInvokeResponse); + break; + + case "composeExtension/onCardButtonClicked": + result = onTeamsMessagingExtensionCardButtonClicked( + turnContext, + turnContext.getActivity().getValue() + ).thenApply(this::createInvokeResponse); + break; + + case "task/fetch": + result = onTeamsTaskModuleFetch( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), TaskModuleRequest.class) + ).thenApply(this::createInvokeResponse); + break; + + case "task/submit": + result = onTeamsTaskModuleSubmit( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), TaskModuleRequest.class) + ).thenApply(this::createInvokeResponse); + break; + + case "tab/fetch": + result = onTeamsTabFetch( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), TabRequest.class) + ).thenApply(this::createInvokeResponse); + break; + + case "tab/submit": + result = onTeamsTabSubmit( + turnContext, + Serialization.safeGetAs(turnContext.getActivity().getValue(), TabSubmit.class) + ).thenApply(this::createInvokeResponse); + break; + + default: + result = super.onInvokeActivity(turnContext); + break; + } + } + } catch (Throwable t) { + result = new CompletableFuture<>(); + result.completeExceptionally(t); + } + + return result.exceptionally(e -> { + if (e instanceof CompletionException && e.getCause() instanceof InvokeResponseException) { + return ((InvokeResponseException) e.getCause()).createInvokeResponse(); + } else if (e instanceof InvokeResponseException) { + return ((InvokeResponseException) e).createInvokeResponse(); + } + return new InvokeResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, e.getLocalizedMessage()); + }); + } + + /** + * Invoked when a card action invoke activity is received from the connector. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsCardActionInvoke(TurnContext turnContext) { + return notImplemented(); + } + + /** + * Invoked when a signIn invoke activity is received from the connector. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onSignInInvoke(TurnContext turnContext) { + return onTeamsSigninVerifyState(turnContext); + } + + /** + * Invoked when a signIn verify state activity is received from the connector. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsSigninVerifyState(TurnContext turnContext) { + return notImplemented(); + } + + /** + * Invoked when a file consent card activity is received from the connector. + * + * @param turnContext The current TurnContext. + * @param fileConsentCardResponse The response representing the value of the + * invoke activity sent when the user acts on a + * file consent card. + * @return An InvokeResponse depending on the action of the file consent card. + */ + protected CompletableFuture onTeamsFileConsent( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + switch (fileConsentCardResponse.getAction()) { + case "accept": + return onTeamsFileConsentAccept(turnContext, fileConsentCardResponse) + .thenApply(aVoid -> createInvokeResponse(null)); + + case "decline": + return onTeamsFileConsentDecline(turnContext, fileConsentCardResponse) + .thenApply(aVoid -> createInvokeResponse(null)); + + default: + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally( + new InvokeResponseException( + HttpURLConnection.HTTP_BAD_REQUEST, + fileConsentCardResponse.getAction() + " is not a supported Action." + ) + ); + return result; + } + } + + /** + * Invoked when a file consent card is accepted by the user. + * + * @param turnContext The current TurnContext. + * @param fileConsentCardResponse The response representing the value of the + * invoke activity sent when the user accepts a + * file consent card. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsFileConsentAccept( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + return notImplemented(); + } + + /** + * Invoked when a file consent card is declined by the user. + * + * @param turnContext The current TurnContext. + * @param fileConsentCardResponse The response representing the value of the + * invoke activity sent when the user declines a + * file consent card. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsFileConsentDecline( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + return notImplemented(); + } + + /** + * Invoked when a Messaging Extension Query activity is received from the + * connector. + * + * @param turnContext The current TurnContext. + * @param query The query for the search command. + * @return The Messaging Extension Response for the query. + */ + protected CompletableFuture onTeamsMessagingExtensionQuery( + TurnContext turnContext, + MessagingExtensionQuery query + ) { + return notImplemented(); + } + + /** + * Invoked when a O365 Connector Card Action activity is received from the + * connector. + * + * @param turnContext The current TurnContext. + * @param query The O365 connector card HttpPOST invoke query. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsO365ConnectorCardAction( + TurnContext turnContext, + O365ConnectorCardActionQuery query + ) { + return notImplemented(); + } + + /** + * Invoked when an app based link query activity is received from the connector. + * + * @param turnContext The current TurnContext. + * @param query The invoke request body type for app-based link query. + * @return The Messaging Extension Response for the query. + */ + protected CompletableFuture onTeamsAppBasedLinkQuery( + TurnContext turnContext, + AppBasedLinkQuery query + ) { + return notImplemented(); + } + + /** + * Invoked when a messaging extension select item activity is received from the + * connector. + * + * @param turnContext The current TurnContext. + * @param query The object representing the query. + * @return The Messaging Extension Response for the query. + */ + protected CompletableFuture onTeamsMessagingExtensionSelectItem( + TurnContext turnContext, + Object query + ) { + return notImplemented(); + } + + /** + * Invoked when a Messaging Extension Fetch activity is received from the + * connector. + * + * @param turnContext The current TurnContext. + * @param action The messaging extension action. + * @return The Messaging Extension Action Response for the action. + */ + protected CompletableFuture onTeamsMessagingExtensionFetchTask( + TurnContext turnContext, + MessagingExtensionAction action + ) { + return notImplemented(); + } + + /** + * Invoked when a messaging extension submit action dispatch activity is + * received from the connector. + * + * @param turnContext The current TurnContext. + * @param action The messaging extension action. + * @return The Messaging Extension Action Response for the action. + */ + protected CompletableFuture onTeamsMessagingExtensionSubmitActionDispatch( + TurnContext turnContext, + MessagingExtensionAction action + ) { + if (!StringUtils.isEmpty(action.getBotMessagePreviewAction())) { + switch (action.getBotMessagePreviewAction()) { + case "edit": + return onTeamsMessagingExtensionBotMessagePreviewEdit(turnContext, action); + + case "send": + return onTeamsMessagingExtensionBotMessagePreviewSend(turnContext, action); + + default: + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally( + new InvokeResponseException( + HttpURLConnection.HTTP_BAD_REQUEST, + action.getBotMessagePreviewAction() + " is not a support BotMessagePreviewAction" + ) + ); + return result; + } + } else { + return onTeamsMessagingExtensionSubmitAction(turnContext, action); + } + } + + /** + * Invoked when a messaging extension submit action activity is received from + * the connector. + * + * @param turnContext The current TurnContext. + * @param action The messaging extension action. + * @return The Messaging Extension Action Response for the action. + */ + protected CompletableFuture onTeamsMessagingExtensionSubmitAction( + TurnContext turnContext, + MessagingExtensionAction action + ) { + return notImplemented(); + } + + /** + * Invoked when a messaging extension bot message preview edit activity is + * received from the connector. + * + * @param turnContext The current TurnContext. + * @param action The messaging extension action. + * @return The Messaging Extension Action Response for the action. + */ + protected CompletableFuture onTeamsMessagingExtensionBotMessagePreviewEdit( + TurnContext turnContext, + MessagingExtensionAction action + ) { + return notImplemented(); + } + + /** + * Invoked when a messaging extension bot message preview send activity is + * received from the connector. + * + * @param turnContext The current TurnContext. + * @param action The messaging extension action. + * @return The Messaging Extension Action Response for the action. + */ + protected CompletableFuture onTeamsMessagingExtensionBotMessagePreviewSend( + TurnContext turnContext, + MessagingExtensionAction action + ) { + return notImplemented(); + } + + /** + * Invoked when a messaging extension configuration query setting url activity + * is received from the connector. + * + * @param turnContext The current TurnContext. + * @param query The Messaging extension query. + * @return The Messaging Extension Response for the query. + */ + protected CompletableFuture onTeamsMessagingExtensionConfigurationQuerySettingUrl( + TurnContext turnContext, + MessagingExtensionQuery query + ) { + return notImplemented(); + } + + /** + * Override this in a derived class to provide logic for when a configuration is + * set for a messaging extension. + * + * @param turnContext The current TurnContext. + * @param settings Object representing the configuration settings. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMessagingExtensionConfigurationSetting( + TurnContext turnContext, + Object settings + ) { + return notImplemented(); + } + + /** + * Override this in a derived class to provide logic for when a task module is + * fetched. + * + * @param turnContext The current TurnContext. + * @param taskModuleRequest The task module invoke request value payload. + * @return A Task Module Response for the request. + */ + protected CompletableFuture onTeamsTaskModuleFetch( + TurnContext turnContext, + TaskModuleRequest taskModuleRequest + ) { + return notImplemented(); + } + + /** + * Override this in a derived class to provide logic for when a card button is + * clicked in a messaging extension. + * + * @param turnContext The current TurnContext. + * @param cardData Object representing the card data. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMessagingExtensionCardButtonClicked( + TurnContext turnContext, + Object cardData + ) { + return notImplemented(); + } + + /** + * Override this in a derived class to provide logic for when a task module is + * submited. + * + * @param turnContext The current TurnContext. + * @param taskModuleRequest The task module invoke request value payload. + * @return A Task Module Response for the request. + */ + protected CompletableFuture onTeamsTaskModuleSubmit( + TurnContext turnContext, + TaskModuleRequest taskModuleRequest + ) { + return notImplemented(); + } + + /** + * Invoked when a conversation update activity is received from the channel. + * Conversation update activities are useful when it comes to responding to + * users being added to or removed from the channel. For example, a bot could + * respond to a user being added by greeting the user. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onConversationUpdateActivity(TurnContext turnContext) { + if (turnContext.getActivity().isTeamsActivity()) { + ResultPair channelData = + turnContext.getActivity().tryGetChannelData(TeamsChannelData.class); + + if (turnContext.getActivity().getMembersAdded() != null) { + return onTeamsMembersAddedDispatch( + turnContext.getActivity().getMembersAdded(), + channelData.result() ? channelData.value().getTeam() : null, + turnContext + ); + } + + if (turnContext.getActivity().getMembersRemoved() != null) { + return onTeamsMembersRemovedDispatch( + turnContext.getActivity().getMembersRemoved(), + channelData.result() ? channelData.value().getTeam() : null, + turnContext + ); + } + + if (channelData.result()) { + switch (channelData.value().getEventType()) { + case "channelCreated": + return onTeamsChannelCreated( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "channelDeleted": + return onTeamsChannelDeleted( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "channelRenamed": + return onTeamsChannelRenamed( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "channelRestored": + return onTeamsChannelRestored( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "teamArchived": + return onTeamsTeamArchived( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "teamDeleted": + return onTeamsTeamDeleted( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "teamHardDeleted": + return onTeamsTeamHardDeleted( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "teamRenamed": + return onTeamsTeamRenamed( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "teamRestored": + return onTeamsTeamRestored( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + case "teamUnarchived": + return onTeamsTeamUnarchived( + channelData.value().getChannel(), + channelData.value().getTeam(), + turnContext + ); + + default: + return super.onConversationUpdateActivity(turnContext); + } + } + } + + return super.onConversationUpdateActivity(turnContext); + } + + /** + * Override this in a derived class to provide logic for when members other than + * the bot join the channel, such as your bot's welcome logic. It will get the + * associated members with the provided accounts. + * + * @param membersAdded A list of all the accounts added to the channel, as + * described by the conversation update activity. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMembersAddedDispatch( + List membersAdded, + TeamInfo teamInfo, + TurnContext turnContext + ) { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + mapper.findAndRegisterModules(); + + List teamsMembersAdded = new ArrayList<>(); + for (ChannelAccount memberAdded : membersAdded) { + boolean isBot = turnContext.getActivity().getRecipient() != null + && StringUtils.equals(memberAdded.getId(), turnContext.getActivity().getRecipient().getId()); + + if (!memberAdded.getProperties().isEmpty() || isBot) { + // when the ChannelAccount object is fully a TeamsChannelAccount, or for the bot + // (when Teams changes the service to return the full details) + try { + JsonNode node = mapper.valueToTree(memberAdded); + teamsMembersAdded.add(mapper.treeToValue(node, TeamsChannelAccount.class)); + } catch (JsonProcessingException jpe) { + return withException(jpe); + } + } else { + TeamsChannelAccount teamsChannelAccount = null; + try { + teamsChannelAccount = TeamsInfo.getMember(turnContext, memberAdded.getId()).join(); + } catch (CompletionException ex) { + Throwable causeException = ex.getCause(); + if (causeException instanceof ErrorResponseException) { + ErrorResponse response = ((ErrorResponseException) causeException).body(); + if (response != null) { + Error error = response.getError(); + if (error != null && !error.getCode().equals("ConversationNotFound")) { + throw ex; + } + } + } else { + throw ex; + } + } + + if (teamsChannelAccount == null) { + // unable to find the member added in ConversationUpdate Activity in + // the response from the getMember call + teamsChannelAccount = new TeamsChannelAccount(); + teamsChannelAccount.setId(memberAdded.getId()); + teamsChannelAccount.setName(memberAdded.getName()); + teamsChannelAccount.setAadObjectId(memberAdded.getAadObjectId()); + teamsChannelAccount.setRole(memberAdded.getRole()); + } + teamsMembersAdded.add(teamsChannelAccount); + } + } + + return onTeamsMembersAdded(teamsMembersAdded, teamInfo, turnContext); + } + + /** + * Override this in a derived class to provide logic for when members other than + * the bot leave the channel, such as your bot's good-bye logic. It will get the + * associated members with the provided accounts. + * + * @param membersRemoved A list of all the accounts removed from the channel, as + * described by the conversation update activity. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMembersRemovedDispatch( + List membersRemoved, + TeamInfo teamInfo, + TurnContext turnContext + ) { + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + mapper.findAndRegisterModules(); + + List teamsMembersRemoved = new ArrayList<>(); + for (ChannelAccount memberRemoved : membersRemoved) { + try { + JsonNode node = mapper.valueToTree(memberRemoved); + teamsMembersRemoved.add(mapper.treeToValue(node, TeamsChannelAccount.class)); + } catch (JsonProcessingException jpe) { + return withException(jpe); + } + } + + return onTeamsMembersRemoved(teamsMembersRemoved, teamInfo, turnContext); + } + + /** + * Override this in a derived class to provide logic for when members other than + * the bot join the channel, such as your bot's welcome logic. + * + * @param membersAdded A list of all the members added to the channel, as + * described by the conversation update activity. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMembersAdded( + List membersAdded, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return onMembersAdded(new ArrayList<>(membersAdded), turnContext); + } + + /** + * Override this in a derived class to provide logic for when members other than + * the bot leave the channel, such as your bot's good-bye logic. + * + * @param membersRemoved A list of all the members removed from the channel, as + * described by the conversation update activity. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMembersRemoved( + List membersRemoved, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return onMembersRemoved(new ArrayList<>(membersRemoved), turnContext); + } + + /** + * Invoked when a Channel Created event activity is received from the connector. + * Channel Created correspond to the user creating a new channel. + * + * @param channelInfo The channel info object which describes the channel. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsChannelCreated( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Channel Deleted event activity is received from the connector. + * Channel Deleted correspond to the user deleting an existing channel. + * + * @param channelInfo The channel info object which describes the channel. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsChannelDeleted( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Channel Renamed event activity is received from the connector. + * Channel Renamed correspond to the user renaming an existing channel. + * + * @param channelInfo The channel info object which describes the channel. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsChannelRenamed( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Channel Restored event activity is received from the + * connector. Channel Restored correspond to the user restoring a previously + * deleted channel. + * + * @param channelInfo The channel info object which describes the channel. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsChannelRestored( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Team Archived event activity is received from the connector. + * Team Archived correspond to the user archiving a team. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsTeamArchived( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Team Deleted event activity is received from the connector. + * Team Deleted correspond to the user deleting a team. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsTeamDeleted( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Team Hard Deleted event activity is received from the + * connector. Team Hard Deleted correspond to the user hard-deleting a team. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsTeamHardDeleted( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Channel Renamed event activity is received from the connector. + * Channel Renamed correspond to the user renaming an existing channel. + * + * @param channelInfo The channel info object which describes the channel. + * @param teamInfo The team info object representing the team. + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsTeamRenamed( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Team Restored event activity is received from the connector. + * Team Restored correspond to the user restoring a team. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsTeamRestored( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Team Unarchived event activity is received from the connector. + * Team Unarchived correspond to the user unarchiving a team. + * + * @param turnContext The current TurnContext. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsTeamUnarchived( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + return CompletableFuture.completedFuture(null); + } + + /** + * Override this in a derived class to provide logic for when a tab is fetched. + * + * @param turnContext The context object for this turn. + * @param tabRequest The tab invoke request value payload. + * @return A Tab Response for the request. + */ + protected CompletableFuture onTeamsTabFetch(TurnContext turnContext, TabRequest tabRequest) { + return withException(new InvokeResponseException(HttpURLConnection.HTTP_NOT_IMPLEMENTED)); + } + + /** + * Override this in a derived class to provide logic for when a tab is + * submitted. + * + * @param turnContext The context object for this turn. + * @param tabSubmit The tab submit invoke request value payload. + * @return A Tab Response for the request. + */ + protected CompletableFuture onTeamsTabSubmit(TurnContext turnContext, TabSubmit tabSubmit) { + return withException(new InvokeResponseException(HttpURLConnection.HTTP_NOT_IMPLEMENTED)); + } + + /** + * Invoked when a "tokens/response" event is received when the base behavior of + * {@link #onEventActivity(TurnContext)} is used. + * + *

+ * If using an OAuthPrompt, override this method to forward this + * {@link com.microsoft.bot.schema.Activity} to the current dialog. + *

+ * + *

+ * By default, this method does nothing. + *

+ *

+ * When the {@link #onEventActivity(TurnContext)} method receives an event with + * a {@link com.microsoft.bot.schema.Activity#getName()} of `tokens/response`, it calls this method. + * + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + @Override + protected CompletableFuture onEventActivity(TurnContext turnContext) { + if (StringUtils.equals(turnContext.getActivity().getChannelId(), Channels.MSTEAMS)) { + try { + switch (turnContext.getActivity().getName()) { + case "application/vnd.microsoft.meetingStart": + return onTeamsMeetingStart( + Serialization.safeGetAs( + turnContext.getActivity().getValue(), + MeetingStartEventDetails.class + ), + turnContext + ); + + case "application/vnd.microsoft.meetingEnd": + return onTeamsMeetingEnd( + Serialization.safeGetAs( + turnContext.getActivity().getValue(), + MeetingEndEventDetails.class + ), + turnContext + ); + + default: + break; + } + } catch (Throwable t) { + return Async.completeExceptionally(t); + } + } + + return super.onEventActivity(turnContext); + } + + /** + * Invoked when a Teams Meeting Start event activity is received from the + * connector. Override this in a derived class to provide logic for when a + * meeting is started. + * + * @param meeting The details of the meeting. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMeetingStart(MeetingStartEventDetails meeting, TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoked when a Teams Meeting End event activity is received from the + * connector. Override this in a derived class to provide logic for when a + * meeting is ended. + * + * @param meeting The details of the meeting. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onTeamsMeetingEnd(MeetingEndEventDetails meeting, TurnContext turnContext) { + return CompletableFuture.completedFuture(null); + } + + /** + * Invoke a new InvokeResponseException with a HTTP 501 code status. + * + * @return true if this invocation caused this CompletableFuture to transition + * to a completed state, else false + */ + protected CompletableFuture notImplemented() { + return notImplemented(null); + } + + /** + * Invoke a new InvokeResponseException with a HTTP 501 code status. + * + * @param body The body for the InvokeResponseException. + * @return true if this invocation caused this CompletableFuture to transition + * to a completed state, else false + */ + protected CompletableFuture notImplemented(String body) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new InvokeResponseException(HttpURLConnection.HTTP_NOT_IMPLEMENTED, body)); + return result; + } + + /** + * Error handler that can catch exceptions. + * + * @param t The exception thrown. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture withException(Throwable t) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new CompletionException(t)); + return result; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java new file mode 100644 index 000000000..f5d96da44 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java @@ -0,0 +1,437 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.microsoft.bot.builder.BotFrameworkAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.rest.RestTeamsConnectorClient; +import com.microsoft.bot.connector.teams.TeamsConnectorClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.PagedMembersResult; +import com.microsoft.bot.schema.Pair; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.teams.ChannelInfo; +import com.microsoft.bot.schema.teams.ConversationList; +import com.microsoft.bot.schema.teams.MeetingInfo; +import com.microsoft.bot.schema.teams.TeamDetails; +import com.microsoft.bot.schema.teams.TeamsChannelAccount; +import com.microsoft.bot.schema.teams.TeamsChannelData; +import com.microsoft.bot.schema.teams.TeamsPagedMembersResult; +import com.microsoft.bot.schema.teams.TeamsMeetingParticipant; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * Teams helper methods. + */ +@SuppressWarnings({ "checkstyle:JavadocMethod" }) +public final class TeamsInfo { + private TeamsInfo() { + } + + /** + * Returns TeamDetails for a Team. + * + * @param turnContext The current TurnContext. + * @param teamId The team id. + * @return A TeamDetails object. + */ + public static CompletableFuture getTeamDetails( + TurnContext turnContext, + String teamId + ) { + String effectiveTeamId = teamId != null + ? teamId + : turnContext.getActivity().teamsGetTeamId(); + if (effectiveTeamId == null) { + return illegalArgument("This method is only valid within the scope of MS Teams Team."); + } + + return getTeamsConnectorClient(turnContext).getTeams().fetchTeamDetails(effectiveTeamId); + } + + /** + * Returns a list of Teams channels. + * + * @param turnContext The current TurnContext. + * @param teamId The team id. + * @return A list of ChannelInfo objects. + */ + public static CompletableFuture> getTeamChannels( + TurnContext turnContext, + String teamId + ) { + String effectiveTeamId = teamId != null + ? teamId + : turnContext.getActivity().teamsGetTeamId(); + if (effectiveTeamId == null) { + return illegalArgument("This method is only valid within the scope of MS Teams Team."); + } + + return getTeamsConnectorClient(turnContext).getTeams().fetchChannelList( + effectiveTeamId + ).thenApply(ConversationList::getConversations); + } + + /** + * Returns a list of team members for the specified team. + * + * @param turnContext The current TurnContext. + * @param teamId The team id. + * @return A list of TeamChannelAccount objects. + */ + public static CompletableFuture> getTeamMembers( + TurnContext turnContext, + String teamId + ) { + String effectiveTeamId = teamId != null + ? teamId + : turnContext.getActivity().teamsGetTeamId(); + if (effectiveTeamId == null) { + return illegalArgument("This method is only valid within the scope of MS Teams Team."); + } + + return getMembers(getConnectorClient(turnContext), effectiveTeamId); + } + + /** + * Returns info for the specified user. + * + * @param turnContext The current TurnContext. + * @param userId The user id. + * @param teamId The team id for the user. + * @return A TeamsChannelAccount for the user, or null if not found. + */ + public static CompletableFuture getTeamMember( + TurnContext turnContext, + String userId, + String teamId + ) { + String effectiveTeamId = teamId != null + ? teamId + : turnContext.getActivity().teamsGetTeamId(); + if (effectiveTeamId == null) { + return illegalArgument("This method is only valid within the scope of MS Teams Team."); + } + + return getMember(getConnectorClient(turnContext), userId, effectiveTeamId); + } + + /** + * Returns a list of members for the current conversation. + * + * @param turnContext The current TurnContext. + * @return A list of TeamsChannelAccount for each member. If this isn't a Teams + * conversation, a list of ChannelAccounts is converted to + * TeamsChannelAccount. + */ + public static CompletableFuture> getMembers(TurnContext turnContext) { + String teamId = turnContext.getActivity().teamsGetTeamId(); + if (!StringUtils.isEmpty(teamId)) { + return getTeamMembers(turnContext, teamId); + } + + String conversationId = turnContext.getActivity().getConversation() != null + ? turnContext.getActivity().getConversation().getId() + : null; + return getMembers(getConnectorClient(turnContext), conversationId); + } + + public static CompletableFuture getMember( + TurnContext turnContext, + String userId + ) { + String teamId = turnContext.getActivity().teamsGetTeamId(); + if (!StringUtils.isEmpty(teamId)) { + return getTeamMember(turnContext, userId, teamId); + } + + String conversationId = turnContext.getActivity().getConversation() != null + ? turnContext.getActivity().getConversation().getId() + : null; + return getMember(getConnectorClient(turnContext), userId, conversationId); + } + + /** + * Returns paged Team member list. + * + * @param turnContext The current TurnContext. + * @param teamId The team id. + * @param continuationToken The continuationToken from a previous call, or null + * the first call. + * @return A TeamsPageMembersResult. + */ + public static CompletableFuture getPagedTeamMembers( + TurnContext turnContext, + String teamId, + String continuationToken + ) { + String effectiveTeamId = teamId != null + ? teamId + : turnContext.getActivity().teamsGetTeamId(); + if (effectiveTeamId == null) { + return illegalArgument("This method is only valid within the scope of MS Teams Team."); + } + + return getPagedMembers(getConnectorClient(turnContext), effectiveTeamId, continuationToken); + } + + /** + * Returns paged Team member list. If the Activity is not from a Teams channel, + * a PagedMembersResult is converted to TeamsPagedMembersResult. + * + * @param turnContext The current TurnContext. + * @param continuationToken The continuationToken from a previous call, or null + * the first call. + * @return A TeamsPageMembersResult. + */ + public static CompletableFuture getPagedMembers( + TurnContext turnContext, + String continuationToken + ) { + String teamId = turnContext.getActivity().teamsGetTeamId(); + if (!StringUtils.isEmpty(teamId)) { + return getPagedTeamMembers(turnContext, teamId, continuationToken); + } + + String conversationId = turnContext.getActivity().getConversation() != null + ? turnContext.getActivity().getConversation().getId() + : null; + return getPagedMembers(getConnectorClient(turnContext), conversationId, continuationToken); + } + + /** + * Gets the details for the given meeting participant. This only works in teams meeting scoped conversations. + * @param turnContext The TurnContext that the meeting, participant, and tenant ids are pulled from. + * @return TeamsParticipantChannelAccount + */ + public static CompletableFuture getMeetingParticipant( + TurnContext turnContext + ) { + return getMeetingParticipant(turnContext, null, null, null); + } + + /** + * Gets the details for the given meeting participant. This only works in teams meeting scoped conversations. + * @param turnContext Turn context. + * @param meetingId The meeting id, or null to get from Activities TeamsChannelData + * @param participantId The participant id, or null to get from Activities TeamsChannelData + * @param tenantId The tenant id, or null to get from Activities TeamsChannelData + * @return Team participant channel account. + */ + public static CompletableFuture getMeetingParticipant( + TurnContext turnContext, + String meetingId, + String participantId, + String tenantId + ) { + if (StringUtils.isEmpty(meetingId)) { + meetingId = turnContext.getActivity().teamsGetMeetingInfo() != null + ? turnContext.getActivity().teamsGetMeetingInfo().getId() + : null; + } + if (StringUtils.isEmpty(meetingId)) { + return illegalArgument("TeamsInfo.getMeetingParticipant: method requires a meetingId"); + } + + if (StringUtils.isEmpty(participantId)) { + participantId = turnContext.getActivity().getFrom() != null + ? turnContext.getActivity().getFrom().getAadObjectId() + : null; + } + if (StringUtils.isEmpty(participantId)) { + return illegalArgument("TeamsInfo.getMeetingParticipant: method requires a participantId"); + } + + if (StringUtils.isEmpty(tenantId)) { + tenantId = turnContext.getActivity().teamsGetChannelData() != null + ? turnContext.getActivity().teamsGetChannelData().getTenant().getId() + : null; + } + if (StringUtils.isEmpty(tenantId)) { + return illegalArgument("TeamsInfo.getMeetingParticipant: method requires a tenantId"); + } + + return getTeamsConnectorClient(turnContext).getTeams().fetchParticipant( + meetingId, + participantId, + tenantId + ); + } + + /** + * Gets the information for the given meeting id. + * @param turnContext Turn context. + * @return Meeting Details. + */ + public static CompletableFuture getMeetingInfo(TurnContext turnContext) { + return getMeetingInfo(turnContext, null); + } + + /** + * Gets the information for the given meeting id. + * @param turnContext Turn context. + * @param meetingId The BASE64-encoded id of the Teams meeting. + * @return Meeting Details. + */ + public static CompletableFuture getMeetingInfo(TurnContext turnContext, String meetingId) { + if (StringUtils.isEmpty(meetingId) && turnContext.getActivity().teamsGetMeetingInfo() != null) { + meetingId = turnContext.getActivity().teamsGetMeetingInfo().getId(); + } + + if (StringUtils.isEmpty(meetingId)) { + return illegalArgument("TeamsInfo.getMeetingInfo: method requires a meetingId"); + } + + return getTeamsConnectorClient(turnContext).getTeams().fetchMeetingInfo(meetingId); + } + + private static CompletableFuture> getMembers( + ConnectorClient connectorClient, + String conversationId + ) { + if (StringUtils.isEmpty(conversationId)) { + return illegalArgument("The getMembers operation needs a valid conversation Id."); + } + + return connectorClient.getConversations().getConversationMembers(conversationId).thenApply( + teamMembers -> { + List members = teamMembers.stream().map( + channelAccount -> Serialization.convert( + channelAccount, + TeamsChannelAccount.class + ) + ).collect(Collectors.toCollection(ArrayList::new)); + + members.removeIf(Objects::isNull); + return members; + } + ); + } + + private static CompletableFuture getMember( + ConnectorClient connectorClient, + String userId, + String conversationId + ) { + if (StringUtils.isEmpty(conversationId) || StringUtils.isEmpty(userId)) { + return illegalArgument( + "The getMember operation needs a valid userId and conversationId." + ); + } + + return connectorClient.getConversations().getConversationMember( + userId, + conversationId + ).thenApply(teamMember -> Serialization.convert(teamMember, TeamsChannelAccount.class)); + } + + private static CompletableFuture getPagedMembers( + ConnectorClient connectorClient, + String conversationId, + String continuationToken + ) { + if (StringUtils.isEmpty(conversationId)) { + return illegalArgument("The getPagedMembers operation needs a valid conversation Id."); + } + + CompletableFuture pagedResult; + if (StringUtils.isEmpty(continuationToken)) { + pagedResult = connectorClient.getConversations().getConversationPagedMembers( + conversationId + ); + } else { + pagedResult = connectorClient.getConversations().getConversationPagedMembers( + conversationId, + continuationToken + ); + } + + // return a converted TeamsPagedMembersResult + return pagedResult.thenApply(TeamsPagedMembersResult::new); + } + + private static ConnectorClient getConnectorClient(TurnContext turnContext) { + ConnectorClient client = turnContext.getTurnState().get( + BotFrameworkAdapter.CONNECTOR_CLIENT_KEY + ); + if (client == null) { + throw new IllegalStateException("This method requires a connector client."); + } + return client; + } + + private static TeamsConnectorClient getTeamsConnectorClient(TurnContext turnContext) { + // for testing to be able to provide a custom client. + TeamsConnectorClient teamsClient = turnContext.getTurnState().get( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY + ); + if (teamsClient != null) { + return teamsClient; + } + + ConnectorClient client = getConnectorClient(turnContext); + return new RestTeamsConnectorClient(client.baseUrl(), client.credentials()); + } + + public static CompletableFuture> sendMessageToTeamsChannel( + TurnContext turnContext, + Activity activity, + String teamsChannelId, + MicrosoftAppCredentials credentials + ) { + if (turnContext == null) { + return illegalArgument("turnContext is required"); + } + if (turnContext.getActivity() == null) { + return illegalArgument("turnContext.Activity is required"); + } + if (StringUtils.isEmpty(teamsChannelId)) { + return illegalArgument("teamsChannelId is required"); + } + if (credentials == null) { + return illegalArgument("credentials is required"); + } + + AtomicReference conversationReference = new AtomicReference<>(); + AtomicReference newActivityId = new AtomicReference<>(); + String serviceUrl = turnContext.getActivity().getServiceUrl(); + TeamsChannelData teamsChannelData = new TeamsChannelData(); + teamsChannelData.setChannel(new ChannelInfo(teamsChannelId)); + + ConversationParameters conversationParameters = new ConversationParameters(); + conversationParameters.setIsGroup(true); + + conversationParameters.setChannelData(teamsChannelData); + conversationParameters.setActivity(activity); + + return ((BotFrameworkAdapter) turnContext.getAdapter()).createConversation( + teamsChannelId, + serviceUrl, + credentials, + conversationParameters, + (TurnContext context) -> { + conversationReference.set(context.getActivity().getConversationReference()); + newActivityId.set(context.getActivity().getId()); + return CompletableFuture.completedFuture(null); + } + ).thenApply(aVoid -> new Pair<>(conversationReference.get(), newActivityId.get())); + } + + private static CompletableFuture illegalArgument(String message) { + CompletableFuture detailResult = new CompletableFuture<>(); + detailResult.completeExceptionally(new IllegalArgumentException(message)); + return detailResult; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsSSOTokenExchangeMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsSSOTokenExchangeMiddleware.java new file mode 100644 index 000000000..78f9654a5 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsSSOTokenExchangeMiddleware.java @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder.teams; + +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.builder.Middleware; +import com.microsoft.bot.builder.NextDelegate; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.StoreItem; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserTokenProvider; +import com.microsoft.bot.connector.rest.RestOAuthClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.SignInConstants; +import com.microsoft.bot.schema.TokenExchangeInvokeRequest; +import com.microsoft.bot.schema.TokenExchangeInvokeResponse; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; + +import org.apache.commons.lang3.StringUtils; + +/** + * If the activity name is signin/tokenExchange, this middleware will attempt + * toexchange the token, and deduplicate the incoming call, ensuring only + * oneexchange request is processed. + * + * If a user is signed into multiple Teams clients, the Bot could receive a + * "signin/tokenExchange" from each client. Each token exchange request for a + * specific user login will have an identical Activity.getValue().getId(). Only + * one of these token exchange requests should be processed by the bot.The + * others return PreconditionFailed. For a distributed bot in production, this + * requires a distributed storage ensuring only one token exchange is processed. + * This middleware supports CosmosDb storage found in + * Microsoft.getBot().getBuilder().getAzure(), or MemoryStorage for local + * development. Storage's ETag implementation for token exchange activity + * deduplication. + */ +public class TeamsSSOTokenExchangeMiddleware implements Middleware { + + private final Storage storage; + private final String oAuthConnectionName; + + /** + * Initializes a new instance of the {@link TeamsSSOTokenExchangeMiddleware} + * class. + * + * @param storage The {@link Storage} to use for deduplication. + * @param connectionName The connection name to use for the single sign on token + * exchange. + */ + public TeamsSSOTokenExchangeMiddleware(Storage storage, String connectionName) { + if (storage == null) { + throw new IllegalArgumentException("storage cannot be null."); + } + + if (StringUtils.isBlank(connectionName)) { + throw new IllegalArgumentException("connectionName cannot be null."); + } + + this.oAuthConnectionName = connectionName; + this.storage = storage; + } + + /** + * Processes an incoming activity. + * + * @param turnContext The context object for this turn. + * @param next The delegate to call to continue the bot middleware + * pipeline. + * @return A task that represents the work queued to execute. Middleware calls + * the {@code next} delegate to pass control to the next middleware in + * the pipeline. If middleware doesn’t call the next delegate, the + * adapter does not call any of the subsequent middleware’s request + * handlers or the bot’s receive handler, and the pipeline short + * circuits. + *

+ * The {@code context} provides information about the incoming activity, + * and other data needed to process the activity. + *

+ *

+ * {@link TurnContext} {@link com.microsoft.bot.schema.Activity} + */ + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + if (turnContext.getActivity() != null && turnContext.getActivity().getName() != null + && turnContext.getActivity().getName().equals(SignInConstants.TOKEN_EXCHANGE_OPERATION_NAME)) { + // If the TokenExchange is NOT successful, the response will have + // already been sent by ExchangedTokenAsync + if (!this.exchangedToken(turnContext).join()) { + return CompletableFuture.completedFuture(null); + } + + // Only one token exchange should proceed from here. Deduplication is performed + // second because in the case of failure due to consent required, every caller + // needs to receive the + if (!deDuplicatedTokenExchangeId(turnContext).join()) { + // If the token is not exchangeable, do not process this activity further. + return CompletableFuture.completedFuture(null); + } + } + + return next.next(); + } + + private CompletableFuture deDuplicatedTokenExchangeId(TurnContext turnContext) { + + // Create a StoreItem with Etag of the unique 'signin/tokenExchange' request + String idValue = null; + TokenStoreItem storeItem = new TokenStoreItem(); + TokenExchangeInvokeRequest tokenExchangeRequest = Serialization.getAs(turnContext.getActivity().getValue(), + TokenExchangeInvokeRequest.class); + if (tokenExchangeRequest != null) { + idValue = tokenExchangeRequest.getId(); + } + + storeItem.setETag(idValue); + + Map storeItems = new HashMap(); + storeItems.put(storeItem.getStorageKey(turnContext), storeItem); + try { + // Writing the StoreItem with ETag of unique id will succeed only once + storage.write(storeItems).join(); + } catch (Exception ex) { + + // Memory storage throws a generic exception with a Message of 'etag conflict. + // [other error info]' + // CosmosDbPartitionedStorage throws: RuntimeException with a message that contains "precondition is + // not met") + if (ex.getMessage().contains("eTag conflict") || ex.getMessage().contains("precondition is not met")) { + // Do NOT proceed processing this message, some other thread or + // machine already has processed it. + + // Send 200 invoke response. + return sendInvokeResponse(turnContext, null, HttpURLConnection.HTTP_OK).thenApply(result -> false); + } + } + + return CompletableFuture.completedFuture(true); + } + + private CompletableFuture sendInvokeResponse(TurnContext turnContext, Object body, int statusCode) { + Activity activity = new Activity(ActivityTypes.INVOKE_RESPONSE); + InvokeResponse response = new InvokeResponse(statusCode, body); + activity.setValue(response); + return turnContext.sendActivity(activity).thenApply(result -> null); + } + + @SuppressWarnings("PMD.EmptyCatchBlock") + private CompletableFuture exchangedToken(TurnContext turnContext) { + TokenResponse tokenExchangeResponse = null; + TokenExchangeInvokeRequest tokenExchangeRequest = Serialization.getAs(turnContext.getActivity().getValue(), + TokenExchangeInvokeRequest.class); + + try { + RestOAuthClient userTokenClient = turnContext.getTurnState().get(RestOAuthClient.class); + TokenExchangeRequest exchangeRequest = new TokenExchangeRequest(); + exchangeRequest.setToken(tokenExchangeRequest.getToken()); + if (userTokenClient != null) { + tokenExchangeResponse = userTokenClient.getUserToken() + .exchangeToken(turnContext.getActivity().getFrom().getId(), oAuthConnectionName, + turnContext.getActivity().getChannelId(), exchangeRequest) + .join(); + } else if (turnContext.getAdapter() instanceof UserTokenProvider) { + UserTokenProvider adapter = (UserTokenProvider) turnContext.getAdapter(); + tokenExchangeResponse = adapter.exchangeToken(turnContext, oAuthConnectionName, + turnContext.getActivity().getFrom().getId(), exchangeRequest).join(); + } else { + throw new RuntimeException("Token Exchange is not supported by the current adapter."); + } + } catch (Exception ex) { + // Ignore Exceptions + // If token exchange failed for any reason, tokenExchangeResponse above stays + // null, + // and hence we send back a failure invoke response to the caller. + } + + if (tokenExchangeResponse != null && StringUtils.isEmpty(tokenExchangeResponse.getToken())) { + // The token could not be exchanged (which could be due to a consent + // requirement) + // Notify the sender that PreconditionFailed so they can respond accordingly. + + TokenExchangeInvokeResponse invokeResponse = new TokenExchangeInvokeResponse(); + invokeResponse.setId(tokenExchangeRequest.getId()); + invokeResponse.setConnectionName(oAuthConnectionName); + invokeResponse.setFailureDetail("The bot is unable to exchange token. Proceed with regular login."); + + sendInvokeResponse(turnContext, invokeResponse, HttpURLConnection.HTTP_PRECON_FAILED); + + return CompletableFuture.completedFuture(false); + } + + return CompletableFuture.completedFuture(true); + } + + /** + * Class to store the etag for token exchange. + */ + private class TokenStoreItem implements StoreItem { + + private String etag; + + @Override + public String getETag() { + return etag; + } + + @Override + public void setETag(String withETag) { + etag = withETag; + } + + public String getStorageKey(TurnContext turnContext) { + Activity activity = turnContext.getActivity(); + if (activity.getChannelId() == null) { + throw new RuntimeException("invalid activity-missing channelId"); + } + if (activity.getConversation() == null || activity.getConversation().getId() == null) { + throw new RuntimeException("invalid activity-missing Conversation.Id"); + } + + String channelId = activity.getChannelId(); + String conversationId = activity.getConversation().getId(); + + TokenExchangeInvokeRequest tokenExchangeRequest = Serialization.getAs(turnContext.getActivity().getValue(), + TokenExchangeInvokeRequest.class); + + if (tokenExchangeRequest != null) { + return String.format("%s/%s/%s", channelId, conversationId, tokenExchangeRequest.getId()); + } else { + throw new RuntimeException("Invalid signin/tokenExchange. Missing activity.getValue().getId()."); + } + } + } + +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/package-info.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/package-info.java new file mode 100644 index 000000000..9080ec831 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.builder.teams. + */ +package com.microsoft.bot.builder.teams; diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActionDel.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActionDel.java new file mode 100644 index 000000000..be6dc43cd --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActionDel.java @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +// This is a proxy for some previous tests using Action bindings +@FunctionalInterface +public interface ActionDel { + void CallMe(); +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java new file mode 100644 index 000000000..a5c41c686 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ActivityHandlerTests.java @@ -0,0 +1,591 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.*; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class ActivityHandlerTests { + @Test + public void TestMessageActivity() { + Activity activity = MessageFactory.text("hello"); + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onMessageActivity", bot.getRecord().get(0)); + } + + @Test + public void TestOnInstallationUpdate() { + Activity activity = new Activity(ActivityTypes.INSTALLATION_UPDATE); + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onInstallationUpdate", bot.getRecord().get(0)); + } + + @Test + public void TestInstallationUpdateAdd() { + Activity activity = new Activity(ActivityTypes.INSTALLATION_UPDATE); + activity.setAction("add"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onInstallationUpdate", bot.getRecord().get(0)); + Assert.assertEquals("onInstallationUpdateAdd", bot.getRecord().get(1)); + } + + @Test + public void TestInstallationUpdateAddUpgrade() { + Activity activity = new Activity(ActivityTypes.INSTALLATION_UPDATE); + activity.setAction("add-upgrade"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onInstallationUpdate", bot.getRecord().get(0)); + Assert.assertEquals("onInstallationUpdateAdd", bot.getRecord().get(1)); + } + + @Test + public void TestInstallationUpdateRemove() { + Activity activity = new Activity(ActivityTypes.INSTALLATION_UPDATE); + activity.setAction("remove"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onInstallationUpdate", bot.getRecord().get(0)); + Assert.assertEquals("onInstallationUpdateRemove", bot.getRecord().get(1)); + } + + @Test + public void TestInstallationUpdateRemoveUpgrade() { + Activity activity = new Activity(ActivityTypes.INSTALLATION_UPDATE); + activity.setAction("remove-upgrade"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onInstallationUpdate", bot.getRecord().get(0)); + Assert.assertEquals("onInstallationUpdateRemove", bot.getRecord().get(1)); + } + + @Test + public void TestOnAdaptiveCardInvoke() { + + AdaptiveCardInvokeValue adaptiveCardInvokeValue = new AdaptiveCardInvokeValue(); + AdaptiveCardInvokeAction adaptiveCardInvokeAction = new AdaptiveCardInvokeAction(); + adaptiveCardInvokeAction.setType("Action.Execute"); + adaptiveCardInvokeValue.setAction(adaptiveCardInvokeAction); + + JsonNode node = Serialization.objectToTree(adaptiveCardInvokeValue); + Activity activity = new Activity() { + { + setType(ActivityTypes.INVOKE); + setName("adaptiveCard/action"); + setValue(node); + } + }; + + TurnContext turnContext = new TurnContextImpl(new TestInvokeAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onInvokeActivity", bot.getRecord().get(0)); + Assert.assertEquals("onAdaptiveCardInvoke", bot.getRecord().get(1)); + } + + @Test + public void TestOnTypingActivity() { + Activity activity = new Activity(ActivityTypes.TYPING); + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onTypingActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberAdded1() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("b")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberAdded2() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + activity.setType(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("b")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersAdded", bot.getRecord().get(1)); + } + + @Test + public void TestMemberAdded3() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("b")); + members.add(new ChannelAccount("c")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersAdded", bot.getRecord().get(1)); + } + + @Test + public void TestMemberRemoved1() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberRemoved2() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersRemoved", bot.getRecord().get(1)); + } + + @Test + public void TestMemberRemoved3() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("b")); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersRemoved", bot.getRecord().get(1)); + } + + @Test + public void TestMemberAddedJustTheBot() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("b")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberRemovedJustTheBot() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMessageReaction() { + // Note the code supports multiple adds and removes in the same activity though + // a channel may decide to send separate activities for each. For example, Teams + // sends separate activities each with a single add and a single remove. + + // Arrange + Activity activity = new Activity(ActivityTypes.MESSAGE_REACTION); + ArrayList reactionsAdded = new ArrayList(); + reactionsAdded.add(new MessageReaction("sad")); + activity.setReactionsAdded(reactionsAdded); + ArrayList reactionsRemoved = new ArrayList(); + reactionsRemoved.add(new MessageReaction("angry")); + activity.setReactionsRemoved(reactionsRemoved); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.getRecord().size()); + Assert.assertEquals("onMessageReactionActivity", bot.getRecord().get(0)); + Assert.assertEquals("onReactionsAdded", bot.getRecord().get(1)); + Assert.assertEquals("onReactionsRemoved", bot.getRecord().get(2)); + } + + @Test + public void TestTokenResponseEventAsync() { + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setName("tokens/response"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onEventActivity", bot.getRecord().get(0)); + Assert.assertEquals("onTokenResponseEvent", bot.getRecord().get(1)); + } + + @Test + public void TestEventAsync() { + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setName("some.random.event"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onEventActivity", bot.getRecord().get(0)); + Assert.assertEquals("onEvent", bot.getRecord().get(1)); + } + + @Test + public void TestEventNullNameAsync() { + Activity activity = new Activity(ActivityTypes.EVENT); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onEventActivity", bot.getRecord().get(0)); + Assert.assertEquals("onEvent", bot.getRecord().get(1)); + } + + @Test + public void TestCommandActivityType() { + Activity activity = new Activity(ActivityTypes.COMMAND); + activity.setName("application/test"); + CommandValue commandValue = new CommandValue(); + commandValue.setCommandId("Test"); + commandValue.setData(new Object()); + activity.setValue(commandValue); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(bot.getRecord().size(), 1); + Assert.assertEquals("onCommandActivity", bot.record.get(0)); + } + + @Test + public void TestCommandResultActivityType() { + Activity activity = new Activity(ActivityTypes.COMMAND_RESULT); + activity.setName("application/test"); + CommandResultValue commandValue = new CommandResultValue(); + commandValue.setCommandId("Test"); + commandValue.setData(new Object()); + activity.setValue(commandValue); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(bot.getRecord().size(), 1); + Assert.assertEquals("onCommandResultActivity", bot.record.get(0)); + } + + @Test + public void TestUnrecognizedActivityType() { + Activity activity = new Activity("shall.not.pass"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onUnrecognizedActivityType", bot.getRecord().get(0)); + } + + private class TestInvokeAdapter extends NotImplementedAdapter { + + private Activity activity; + + public Activity getActivity() { + return activity; + } + + public CompletableFuture sendActivities( + TurnContext context, + List activities + ) { + activity = activities.stream() + .filter(x -> x.getType().equals(ActivityTypes.INVOKE_RESPONSE)) + .findFirst() + .get(); + return CompletableFuture.completedFuture(new ResourceResponse[0]); + } + } + + private static class NotImplementedAdapter extends BotAdapter { + @Override + public CompletableFuture sendActivities( + TurnContext context, + List activities + ) { + return Async.completeExceptionally(new RuntimeException()); + } + + @Override + public CompletableFuture updateActivity( + TurnContext context, + Activity activity + ) { + return Async.completeExceptionally(new RuntimeException()); + } + + @Override + public CompletableFuture deleteActivity( + TurnContext context, + ConversationReference reference + ) { + return Async.completeExceptionally(new RuntimeException()); + } + } + + private static class TestActivityHandler extends ActivityHandler { + private List record = new ArrayList<>(); + + public List getRecord() { + return record; + } + + public void setRecord(List record) { + this.record = record; + } + + @Override + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + record.add("onMessageActivity"); + return super.onMessageActivity(turnContext); + } + + @Override + protected CompletableFuture onConversationUpdateActivity(TurnContext turnContext) { + record.add("onConversationUpdateActivity"); + return super.onConversationUpdateActivity(turnContext); + } + + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, + TurnContext turnContext + ) { + record.add("onMembersAdded"); + return super.onMembersAdded(membersAdded, turnContext); + } + + @Override + protected CompletableFuture onMembersRemoved( + List membersRemoved, + TurnContext turnContext + ) { + record.add("onMembersRemoved"); + return super.onMembersRemoved(membersRemoved, turnContext); + } + + @Override + protected CompletableFuture onMessageReactionActivity(TurnContext turnContext) { + record.add("onMessageReactionActivity"); + return super.onMessageReactionActivity(turnContext); + } + + @Override + protected CompletableFuture onReactionsAdded( + List messageReactions, + TurnContext turnContext + ) { + record.add("onReactionsAdded"); + return super.onReactionsAdded(messageReactions, turnContext); + } + + @Override + protected CompletableFuture onReactionsRemoved( + List messageReactions, + TurnContext turnContext + ) { + record.add("onReactionsRemoved"); + return super.onReactionsRemoved(messageReactions, turnContext); + } + + @Override + protected CompletableFuture onEventActivity(TurnContext turnContext) { + record.add("onEventActivity"); + return super.onEventActivity(turnContext); + } + + @Override + protected CompletableFuture onTokenResponseEvent(TurnContext turnContext) { + record.add("onTokenResponseEvent"); + return super.onTokenResponseEvent(turnContext); + } + + @Override + protected CompletableFuture onEvent(TurnContext turnContext) { + record.add("onEvent"); + return super.onEvent(turnContext); + } + + @Override + protected CompletableFuture onInvokeActivity(TurnContext turnContext) { + record.add("onInvokeActivity"); + return super.onInvokeActivity(turnContext); + } + + @Override + protected CompletableFuture onInstallationUpdate(TurnContext turnContext) { + record.add("onInstallationUpdate"); + return super.onInstallationUpdate(turnContext); + } + + @Override + protected CompletableFuture onInstallationUpdateAdd(TurnContext turnContext) { + record.add("onInstallationUpdateAdd"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onInstallationUpdateRemove(TurnContext turnContext) { + record.add("onInstallationUpdateRemove"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTypingActivity(TurnContext turnContext) { + record.add("onTypingActivity"); + return super.onTypingActivity(turnContext); + } + + @Override + protected CompletableFuture onUnrecognizedActivityType(TurnContext turnContext) { + record.add("onUnrecognizedActivityType"); + return super.onUnrecognizedActivityType(turnContext); + } + + @Override + protected CompletableFuture onCommandActivity(TurnContext turnContext){ + record.add("onCommandActivity"); + return super.onCommandActivity(turnContext); + } + + @Override + protected CompletableFuture onCommandResultActivity(TurnContext turnContext) { + record.add("onCommandResultActivity"); + return super.onCommandResultActivity(turnContext); + } + + @Override + protected CompletableFuture onAdaptiveCardInvoke( + TurnContext turnContext, AdaptiveCardInvokeValue invokeValue) { + record.add("onAdaptiveCardInvoke"); + return CompletableFuture.completedFuture(new AdaptiveCardInvokeResponse()); + } + + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/AnonymousReceiveMiddleware.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/AnonymousReceiveMiddleware.java new file mode 100644 index 000000000..d1bae2f76 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/AnonymousReceiveMiddleware.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +/** + * Helper class for defining middleware by using a delegate or anonymous method. + */ +public class AnonymousReceiveMiddleware implements Middleware { + private MiddlewareCall _toCall; + + @FunctionalInterface + public interface MiddlewareCall { + CompletableFuture onTurn(TurnContext tc, NextDelegate nd); + } + + /** + * Creates a middleware object that uses the provided method as its process + * request handler. + * + * @param anonymousMethod The method to use as the middleware's process request + * handler. + */ + public AnonymousReceiveMiddleware(MiddlewareCall anonymousMethod) { + if (anonymousMethod == null) + throw new NullPointerException("MiddlewareCall anonymousMethod"); + else + _toCall = anonymousMethod; + } + + /** + * Uses the method provided in the {@link AnonymousReceiveMiddleware} to process + * an incoming activity. + * + * @param context The context object for this turn. + * @param next The delegate to call to continue the bot middleware pipeline. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + return _toCall.onTurn(context, next); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/AutoSaveStateMiddlewareTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/AutoSaveStateMiddlewareTests.java new file mode 100644 index 000000000..72176400b --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/AutoSaveStateMiddlewareTests.java @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +public class AutoSaveStateMiddlewareTests { + @Test + public void AutoSaveStateMiddleware_DualReadWrite() { + Storage storage = new MemoryStorage(); + + // setup userstate + UserState userState = new UserState(storage); + StatePropertyAccessor userProperty = userState.createProperty("userCount"); + + // setup convState + ConversationState convState = new ConversationState(storage); + StatePropertyAccessor convProperty = convState.createProperty("convCount"); + + TestAdapter adapter = new TestAdapter().use( + new AutoSaveStateMiddleware(userState, convState) + ); + + final int USER_INITIAL_COUNT = 100; + final int CONVERSATION_INITIAL_COUNT = 10; + + BotCallbackHandler botLogic = (turnContext -> { + Integer userCount = userProperty.get(turnContext, () -> USER_INITIAL_COUNT).join(); + Integer convCount = convProperty.get( + turnContext, + () -> CONVERSATION_INITIAL_COUNT + ).join(); + + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + if (StringUtils.equals(turnContext.getActivity().getText(), "get userCount")) { + turnContext.sendActivity( + turnContext.getActivity().createReply(userCount.toString()) + ).join(); + } else if ( + StringUtils.equals(turnContext.getActivity().getText(), "get convCount") + ) { + turnContext.sendActivity( + turnContext.getActivity().createReply(convCount.toString()) + ).join(); + } + } + + // increment userCount and set property using accessor. To be saved later by + // AutoSaveStateMiddleware + userCount++; + userProperty.set(turnContext, userCount).join(); + + // increment convCount and set property using accessor. To be saved later by + // AutoSaveStateMiddleware + convCount++; + convProperty.set(turnContext, convCount).join(); + + return CompletableFuture.completedFuture(null); + }); + + new TestFlow(adapter, botLogic).send("test1").send("get userCount").assertReply( + String.format("%d", USER_INITIAL_COUNT + 1) + ).send("get userCount").assertReply(String.format("%d", USER_INITIAL_COUNT + 2)).send( + "get convCount" + ).assertReply(String.format("%d", CONVERSATION_INITIAL_COUNT + 3)).startTest().join(); + + ConversationReference conversation = new ConversationReference(); + conversation.setChannelId("test"); + conversation.setServiceUrl("https://test.com"); + conversation.setUser(new ChannelAccount("user1", "User1")); + conversation.setBot(new ChannelAccount("bot", "Bot")); + conversation.setConversation(new ConversationAccount(false, "convo2", "Conversation2")); + adapter = new TestAdapter(conversation).use(new AutoSaveStateMiddleware(userState, convState)); + + new TestFlow(adapter, botLogic).send("get userCount").assertReply( + String.format("%d", USER_INITIAL_COUNT + 4) + ).send("get convCount").assertReply( + String.format("%d", CONVERSATION_INITIAL_COUNT + 1) + ).startTest().join(); + } + + @Test + public void AutoSaveStateMiddleware_Chain() { + Storage storage = new MemoryStorage(); + + // setup userstate + UserState userState = new UserState(storage); + StatePropertyAccessor userProperty = userState.createProperty("userCount"); + + // setup convState + ConversationState convState = new ConversationState(storage); + StatePropertyAccessor convProperty = convState.createProperty("convCount"); + + AutoSaveStateMiddleware bss = new AutoSaveStateMiddleware(); + { + bss.add(userState); + bss.add(convState); + + TestAdapter adapter = new TestAdapter().use(bss); + + final int USER_INITIAL_COUNT = 100; + final int CONVERSATION_INITIAL_COUNT = 10; + + BotCallbackHandler botLogic = (turnContext -> { + Integer userCount = userProperty.get(turnContext, () -> USER_INITIAL_COUNT).join(); + Integer convCount = convProperty.get( + turnContext, + () -> CONVERSATION_INITIAL_COUNT + ).join(); + + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + if (StringUtils.equals(turnContext.getActivity().getText(), "get userCount")) { + turnContext.sendActivity( + turnContext.getActivity().createReply(userCount.toString()) + ).join(); + } else if ( + StringUtils.equals(turnContext.getActivity().getText(), "get convCount") + ) { + turnContext.sendActivity( + turnContext.getActivity().createReply(convCount.toString()) + ).join(); + } + } + + // increment userCount and set property using accessor. To be saved later by + // AutoSaveStateMiddleware + userCount++; + userProperty.set(turnContext, userCount).join(); + + // increment convCount and set property using accessor. To be saved later by + // AutoSaveStateMiddleware + convCount++; + convProperty.set(turnContext, convCount).join(); + + return CompletableFuture.completedFuture(null); + }); + + new TestFlow(adapter, botLogic).send("test1").send("get userCount").assertReply( + String.format("%d", USER_INITIAL_COUNT + 1) + ).send("get userCount").assertReply(String.format("%d", USER_INITIAL_COUNT + 2)).send( + "get convCount" + ).assertReply(String.format("%d", CONVERSATION_INITIAL_COUNT + 3)).startTest().join(); + + // new adapter on new conversation + AutoSaveStateMiddleware bss2 = new AutoSaveStateMiddleware(); + bss2.add(userState); + bss2.add(convState); + + ConversationReference conversation = new ConversationReference(); + conversation.setChannelId(Channels.TEST); + conversation.setServiceUrl("https://test.com"); + conversation.setUser(new ChannelAccount("user1", "User1")); + conversation.setBot(new ChannelAccount("bot", "Bot")); + conversation.setConversation(new ConversationAccount(false, "convo2", "Conversation2")); + adapter = new TestAdapter(conversation).use(bss2); + + new TestFlow(adapter, botLogic).send("get userCount").assertReply( + String.format("%d", USER_INITIAL_COUNT + 4) + ).send("get convCount").assertReply( + String.format("%d", CONVERSATION_INITIAL_COUNT + 1) + ).startTest().join(); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterBracketingTest.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterBracketingTest.java new file mode 100644 index 000000000..d15d1b3b9 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterBracketingTest.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import org.junit.Test; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class BotAdapterBracketingTest { + @Test + public void Middleware_BracketingValidation() { + TestAdapter adapter = new TestAdapter().use(new BeforeAfterMiddleware()); + + BotCallbackHandler echo = (turnContext -> { + String toEcho = "ECHO:" + turnContext.getActivity().getText(); + return turnContext.sendActivity( + turnContext.getActivity().createReply(toEcho) + ).thenApply(resourceResponse -> null); + }); + + new TestFlow(adapter, echo).send("test").assertReply("BEFORE").assertReply( + "ECHO:test" + ).assertReply("AFTER").startTest().join(); + } + + @Test + public void Middleware_ThrowException() { + String uniqueId = UUID.randomUUID().toString(); + + TestAdapter adapter = new TestAdapter().use(new CatchExceptionMiddleware()); + + BotCallbackHandler echoWithException = (turnContext -> { + String toEcho = "ECHO:" + turnContext.getActivity().getText(); + return turnContext.sendActivity( + turnContext.getActivity().createReply(toEcho) + ).thenCompose(resourceResponse -> { + CompletableFuture result = new CompletableFuture(); + result.completeExceptionally(new RuntimeException(uniqueId)); + return result; + }); + }); + + new TestFlow(adapter, echoWithException).send("test").assertReply("BEFORE").assertReply( + "ECHO:test" + ).assertReply("CAUGHT: " + uniqueId).assertReply("AFTER").startTest().join(); + } + + private static class CatchExceptionMiddleware implements Middleware { + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + return turnContext.sendActivity( + turnContext.getActivity().createReply("BEFORE") + ).thenCompose(resourceResponse -> next.next()).exceptionally(exception -> { + turnContext.sendActivity( + turnContext.getActivity().createReply( + "CAUGHT: " + exception.getCause().getMessage() + ) + ).join(); + return null; + }).thenCompose( + result -> turnContext.sendActivity(turnContext.getActivity().createReply("AFTER")).thenApply(resourceResponse -> null) + ); + } + } + + private static class BeforeAfterMiddleware implements Middleware { + @Override + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + return turnContext.sendActivity( + turnContext.getActivity().createReply("BEFORE") + ).thenCompose(result -> next.next()).thenCompose( + result -> turnContext.sendActivity(turnContext.getActivity().createReply("AFTER")).thenApply(resourceResponse -> null) + ); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java new file mode 100644 index 000000000..40ba1c971 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotAdapterTests.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.schema.*; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class BotAdapterTests { + @Test + public void AdapterSingleUse() { + SimpleAdapter a = new SimpleAdapter(); + a.use(new CallCountingMiddleware()); + } + + @Test + public void AdapterUseChaining() { + SimpleAdapter a = new SimpleAdapter(); + a.use(new CallCountingMiddleware()).use(new CallCountingMiddleware()); + } + + @Test + public void PassResourceResponsesThrough() { + Consumer> validateResponse = (activities) -> { + // no need to do anything. + }; + + SimpleAdapter a = new SimpleAdapter(validateResponse); + TurnContextImpl c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + String activityId = UUID.randomUUID().toString(); + Activity activity = TestMessage.Message(); + activity.setId(activityId); + + ResourceResponse resourceResponse = c.sendActivity(activity).join(); + Assert.assertTrue( + "Incorrect response Id returned", + StringUtils.equals(resourceResponse.getId(), activityId) + ); + } + + @Test + public void GetLocaleFromActivity() { + Consumer> validateResponse = (activities) -> { + // no need to do anything. + }; + SimpleAdapter a = new SimpleAdapter(validateResponse); + TurnContextImpl c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + String activityId = UUID.randomUUID().toString(); + Activity activity = TestMessage.Message(); + activity.setId(activityId); + activity.setLocale("de-DE"); + BotCallbackHandler callback = (turnContext) -> { + Assert.assertEquals("de-DE", turnContext.getActivity().getLocale()); + return CompletableFuture.completedFuture(null); + }; + + a.processRequest(activity, callback).join(); + } + + + @Test + public void ContinueConversation_DirectMsgAsync() { + boolean[] callbackInvoked = new boolean[] { false }; + + TestAdapter adapter = new TestAdapter(); + ConversationReference cr = new ConversationReference(); + cr.setActivityId("activityId"); + ChannelAccount botAccount = new ChannelAccount(); + botAccount.setId("channelId"); + botAccount.setName("testChannelAccount"); + botAccount.setRole(RoleTypes.BOT); + cr.setBot(botAccount); + cr.setChannelId("testChannel"); + cr.setServiceUrl("testUrl"); + ConversationAccount conversation = new ConversationAccount(); + conversation.setConversationType(""); + conversation.setId("testConversationId"); + conversation.setIsGroup(false); + conversation.setName("testConversationName"); + conversation.setRole(RoleTypes.USER); + cr.setConversation(conversation); + ChannelAccount userAccount = new ChannelAccount(); + userAccount.setId("channelId"); + userAccount.setName("testChannelAccount"); + userAccount.setRole(RoleTypes.BOT); + cr.setUser(userAccount); + + BotCallbackHandler callback = (turnContext) -> { + callbackInvoked[0] = true; + return CompletableFuture.completedFuture(null); + }; + + adapter.continueConversation("MyBot", cr, callback).join(); + Assert.assertTrue(callbackInvoked[0]); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java new file mode 100644 index 000000000..05c007b79 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.CredentialProvider; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.SimpleChannelProvider; +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityEventNames; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.CallerIdConstants; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.ExpectedReplies; +import com.microsoft.bot.schema.ResourceResponse; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import java.util.concurrent.CompletableFuture; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class BotFrameworkAdapterTests { + @Test + public void TenantIdShouldBeSetInConversationForTeams() { + Activity activity = processActivity(Channels.MSTEAMS, "theTenantId", null); + Assert.assertEquals("theTenantId", activity.getConversation().getTenantId()); + } + + @Test + public void TenantIdShouldNotChangeInConversationForTeamsIfPresent() { + Activity activity = processActivity(Channels.MSTEAMS, "theTenantId", "shouldNotBeReplaced"); + Assert.assertEquals("shouldNotBeReplaced", activity.getConversation().getTenantId()); + } + + @Test + public void TenantIdShouldNotBeSetInConversationIfNotTeams() { + Activity activity = processActivity(Channels.DIRECTLINE, "theTenantId", null); + Assert.assertNull(activity.getConversation().getTenantId()); + } + + @Test + public void CreateConversationOverloadProperlySetsTenantId() { + // Arrange + final String ActivityIdValue = "SendActivityId"; + final String ConversationIdValue = "NewConversationId"; + final String TenantIdValue = "theTenantId"; + final String EventActivityName = ActivityEventNames.CREATE_CONVERSATION; + + // so we can provide a mock ConnectorClient. + class TestBotFrameworkAdapter extends BotFrameworkAdapter { + public TestBotFrameworkAdapter(CredentialProvider withCredentialProvider) { + super(withCredentialProvider); + } + + @Override + protected CompletableFuture getOrCreateConnectorClient( + String serviceUrl, + AppCredentials usingAppCredentials + ) { + Conversations conv = mock(Conversations.class); + ConversationResourceResponse response = new ConversationResourceResponse(); + response.setActivityId(ActivityIdValue); + response.setId(ConversationIdValue); + when(conv.createConversation(any())).thenReturn( + CompletableFuture.completedFuture(response) + ); + + ConnectorClient client = mock(ConnectorClient.class); + when(client.getConversations()).thenReturn(conv); + + return CompletableFuture.completedFuture(client); + } + } + + CredentialProvider mockCredentialProvider = mock(CredentialProvider.class); + BotFrameworkAdapter adapter = new TestBotFrameworkAdapter(mockCredentialProvider); + + ObjectNode channelData = JsonNodeFactory.instance.objectNode(); + channelData.set( + "tenant", + JsonNodeFactory.instance.objectNode() + .set("id", JsonNodeFactory.instance.textNode(TenantIdValue)) + ); + + Activity activity = new Activity("Test"); + activity.setChannelId(Channels.MSTEAMS); + activity.setServiceUrl("https://fake.service.url"); + activity.setChannelData(channelData); + ConversationAccount conversation = new ConversationAccount(); + conversation.setTenantId(TenantIdValue); + activity.setConversation(conversation); + + Activity activityConversation = new Activity(ActivityTypes.MESSAGE); + activityConversation.setChannelData(activity.getChannelData()); + ConversationParameters parameters = new ConversationParameters(); + parameters.setActivity(activityConversation); + + ConversationReference reference = activity.getConversationReference(); + MicrosoftAppCredentials credentials = new MicrosoftAppCredentials(null, null); + + Activity[] newActivity = new Activity[] {null}; + BotCallbackHandler updateParameters = (turnContext -> { + newActivity[0] = turnContext.getActivity(); + return CompletableFuture.completedFuture(null); + }); + + adapter.createConversation( + activity.getChannelId(), + activity.getServiceUrl(), + credentials, + parameters, + updateParameters, + reference + ).join(); + + Assert.assertEquals( + TenantIdValue, + ((JsonNode) newActivity[0].getChannelData()).get("tenant").get("tenantId").textValue() + ); + Assert.assertEquals(ActivityIdValue, newActivity[0].getId()); + Assert.assertEquals(ConversationIdValue, newActivity[0].getConversation().getId()); + Assert.assertEquals(TenantIdValue, newActivity[0].getConversation().getTenantId()); + Assert.assertEquals(EventActivityName, newActivity[0].getName()); + } + + private Activity processActivity( + String channelId, + String channelDataTenantId, + String conversationTenantId + ) { + ClaimsIdentity mockClaims = new ClaimsIdentity("anonymous"); + CredentialProvider mockCredentials = new SimpleCredentialProvider(); + + BotFrameworkAdapter sut = new BotFrameworkAdapter(mockCredentials); + + ObjectNode channelData = new ObjectMapper().createObjectNode(); + ObjectNode tenantId = new ObjectMapper().createObjectNode(); + tenantId.put("id", channelDataTenantId); + channelData.set("tenant", tenantId); + + Activity[] activity = new Activity[] {null}; + Activity activityTest = new Activity("test"); + activityTest.setChannelId(channelId); + activityTest.setServiceUrl("https://smba.trafficmanager.net/amer/"); + activityTest.setChannelData(channelData); + ConversationAccount conversation = new ConversationAccount(); + conversation.setTenantId(conversationTenantId); + activityTest.setConversation(conversation); + sut.processActivity(mockClaims, activityTest, (context) -> { + activity[0] = context.getActivity(); + return CompletableFuture.completedFuture(null); + }).join(); + + return activity[0]; + } + + @Test + public void OutgoingActivityIdNotSent() { + CredentialProvider mockCredentials = mock(CredentialProvider.class); + BotFrameworkAdapter adapter = new BotFrameworkAdapter(mockCredentials); + + Activity incoming_activity = new Activity("test"); + incoming_activity.setId("testid"); + incoming_activity.setChannelId(Channels.DIRECTLINE); + incoming_activity.setServiceUrl("https://fake.service.url"); + incoming_activity.setConversation(new ConversationAccount("cid")); + + Activity reply = MessageFactory.text("test"); + reply.setId("TestReplyId"); + + MemoryConnectorClient mockConnector = new MemoryConnectorClient(); + TurnContext turnContext = new TurnContextImpl(adapter, incoming_activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, mockConnector); + turnContext.sendActivity(reply).join(); + + Assert.assertEquals( + 1, + ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size() + ); + Assert.assertNull( + ((MemoryConversations) mockConnector.getConversations()).getSentActivities().get(0).getId() + ); + } + + @Test + public void processActivityCreatesCorrectCredsAndClient_anon() { + processActivityCreatesCorrectCredsAndClient( + null, + null, + null, + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + 0, + 1 + ); + } + + @Test + public void processActivityCreatesCorrectCredsAndClient_public() { + processActivityCreatesCorrectCredsAndClient( + "00000000-0000-0000-0000-000000000001", + CallerIdConstants.PUBLIC_AZURE_CHANNEL, + null, + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + 1, + 1 + ); + } + + @Test + public void processActivityCreatesCorrectCredsAndClient_gov() { + processActivityCreatesCorrectCredsAndClient( + "00000000-0000-0000-0000-000000000001", + CallerIdConstants.US_GOV_CHANNEL, + GovernmentAuthenticationConstants.CHANNELSERVICE, + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + 1, + 1 + ); + } + + private void processActivityCreatesCorrectCredsAndClient( + String botAppId, + String expectedCallerId, + String channelService, + String expectedScope, + int expectedAppCredentialsCount, + int expectedClientCredentialsCount + ) { + HashMap claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, botAppId); + claims.put(AuthenticationConstants.APPID_CLAIM, botAppId); + claims.put(AuthenticationConstants.VERSION_CLAIM, "1.0"); + ClaimsIdentity identity = new ClaimsIdentity("anonymous", claims); + + SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider(); + credentialProvider.setAppId(botAppId); + String serviceUrl = "https://smba.trafficmanager.net/amer/"; + + BotFrameworkAdapter sut = new BotFrameworkAdapter( + credentialProvider, + new SimpleChannelProvider(channelService), + null, + null + ); + + BotCallbackHandler callback = turnContext -> { + getAppCredentialsAndAssertValues( + turnContext, + botAppId, + expectedScope, + expectedAppCredentialsCount + ); + + getConnectorClientAndAssertValues( + turnContext, + botAppId, + expectedScope, + serviceUrl, + expectedClientCredentialsCount + ); + + Assert.assertEquals(expectedCallerId, turnContext.getActivity().getCallerId()); + + return CompletableFuture.completedFuture(null); + }; + + Activity activityTest = new Activity("test"); + activityTest.setChannelId(Channels.EMULATOR); + activityTest.setServiceUrl(serviceUrl); + sut.processActivity(identity, activityTest, callback).join(); + } + + @Test + public void ShouldNotLogContinueConversation() { + TranscriptStore transcriptStore = new MemoryTranscriptStore(); + TranscriptLoggerMiddleware sut = new TranscriptLoggerMiddleware(transcriptStore); + + String conversationId = UUID.randomUUID().toString(); + TestAdapter adapter = new TestAdapter(TestAdapter.createConversationReference(conversationId, "User1", "Bot")) + .use(sut); + + Activity continueConversation = new Activity(ActivityTypes.EVENT); + continueConversation.setName(ActivityEventNames.CONTINUE_CONVERSATION); + + new TestFlow(adapter, turnContext -> { + return turnContext.sendActivity("bar").thenApply(resourceResponse -> null); + }) + .send("foo") + .assertReply(activity -> { + Assert.assertEquals("bar", activity.getText()); + PagedResult activities = transcriptStore.getTranscriptActivities(activity.getChannelId(), conversationId).join(); + Assert.assertEquals(2, activities.getItems().size()); + }) + .send(continueConversation) + .assertReply(activity -> { + // Ensure the event hasn't been added to the transcript. + PagedResult activities = transcriptStore.getTranscriptActivities(activity.getChannelId(), conversationId).join(); + + Assert.assertFalse(activities.getItems().stream().anyMatch(a -> a.isType(ActivityTypes.EVENT) && StringUtils + .equals(a.getName(), ActivityEventNames.CONTINUE_CONVERSATION))); + Assert.assertEquals(3, activities.getItems().size()); + }) + .startTest().join(); + } + + private static void getAppCredentialsAndAssertValues( + TurnContext turnContext, + String expectedAppId, + String expectedScope, + int credsCount + ) { + if (credsCount > 0) { + Map credsCache = + ((BotFrameworkAdapter) turnContext.getAdapter()).getCredentialsCache(); + AppCredentials creds = credsCache + .get(BotFrameworkAdapter.keyForAppCredentials(expectedAppId, expectedScope)); + + Assert + .assertEquals("Unexpected credentials cache count", credsCount, credsCache.size()); + + Assert.assertNotNull("Credentials not found", creds); + Assert.assertEquals("Unexpected app id", expectedAppId, creds.getAppId()); + Assert.assertEquals("Unexpected scope", expectedScope, creds.oAuthScope()); + } + } + + private static void getConnectorClientAndAssertValues( + TurnContext turnContext, + String expectedAppId, + String expectedScope, + String expectedUrl, + int clientCount + ) { + Map clientCache = + ((BotFrameworkAdapter) turnContext.getAdapter()).getConnectorClientCache(); + + String cacheKey = expectedAppId == null + ? BotFrameworkAdapter.keyForConnectorClient(expectedUrl, null, null) + : BotFrameworkAdapter.keyForConnectorClient(expectedUrl, expectedAppId, expectedScope); + ConnectorClient client = clientCache.get(cacheKey); + + Assert.assertNotNull("ConnectorClient not in cache", client); + Assert.assertEquals("Unexpected credentials cache count", clientCount, clientCache.size()); + AppCredentials creds = (AppCredentials) client.credentials(); + Assert.assertEquals( + "Unexpected app id", + expectedAppId, + creds == null ? null : creds.getAppId() + ); + Assert.assertEquals( + "Unexpected scope", + expectedScope, + creds == null ? null : creds.oAuthScope() + ); + Assert.assertEquals("Unexpected base url", expectedUrl, client.baseUrl()); + } + + @Test + public void DeliveryModeExpectReplies() { + BotFrameworkAdapter adapter = new BotFrameworkAdapter(new SimpleCredentialProvider()); + + MockConnectorClient mockConnector = new MockConnectorClient("Windows/3.1", new MockAppCredentials("awesome")); + adapter.addConnectorClientToCache("http://tempuri.org/whatever", null, null, mockConnector); + + BotCallbackHandler callback = turnContext -> { + turnContext.sendActivity(MessageFactory.text("activity 1")).join(); + turnContext.sendActivity(MessageFactory.text("activity 2")).join(); + turnContext.sendActivity(MessageFactory.text("activity 3")).join(); + return CompletableFuture.completedFuture(null); + }; + + Activity inboundActivity = new Activity(ActivityTypes.MESSAGE); + inboundActivity.setChannelId(Channels.EMULATOR); + inboundActivity.setServiceUrl("http://tempuri.org/whatever"); + inboundActivity.setDeliveryMode(DeliveryModes.EXPECT_REPLIES.toString()); + inboundActivity.setText("hello world"); + + InvokeResponse invokeResponse = adapter.processActivity((String) null, inboundActivity, callback).join(); + + Assert.assertEquals((int) HttpURLConnection.HTTP_OK, invokeResponse.getStatus()); + List activities = ((ExpectedReplies)invokeResponse.getBody()).getActivities(); + Assert.assertEquals(3, activities.size()); + Assert.assertEquals("activity 1", activities.get(0).getText()); + Assert.assertEquals("activity 2", activities.get(1).getText()); + Assert.assertEquals("activity 3", activities.get(2).getText()); + Assert.assertEquals(0, ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()); + } + + @Test + public void DeliveryModeNormal() { + BotFrameworkAdapter adapter = new BotFrameworkAdapter(new SimpleCredentialProvider()); + + MockConnectorClient mockConnector = new MockConnectorClient("Windows/3.1", new MockAppCredentials("awesome")); + adapter.addConnectorClientToCache("http://tempuri.org/whatever", null, null, mockConnector); + + BotCallbackHandler callback = turnContext -> { + turnContext.sendActivity(MessageFactory.text("activity 1")).join(); + turnContext.sendActivity(MessageFactory.text("activity 2")).join(); + turnContext.sendActivity(MessageFactory.text("activity 3")).join(); + return CompletableFuture.completedFuture(null); + }; + + Activity inboundActivity = new Activity(ActivityTypes.MESSAGE); + inboundActivity.setChannelId(Channels.EMULATOR); + inboundActivity.setServiceUrl("http://tempuri.org/whatever"); + inboundActivity.setDeliveryMode(DeliveryModes.NORMAL.toString()); + inboundActivity.setText("hello world"); + inboundActivity.setConversation(new ConversationAccount("conversationId")); + + InvokeResponse invokeResponse = adapter.processActivity((String) null, inboundActivity, callback).join(); + + Assert.assertNull(invokeResponse); + Assert.assertEquals(3, ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()); + } + + // should be same as DeliverModes.NORMAL + @Test + public void DeliveryModeNull() { + BotFrameworkAdapter adapter = new BotFrameworkAdapter(new SimpleCredentialProvider()); + + MockConnectorClient mockConnector = new MockConnectorClient("Windows/3.1", new MockAppCredentials("awesome")); + adapter.addConnectorClientToCache("http://tempuri.org/whatever", null, null, mockConnector); + + BotCallbackHandler callback = turnContext -> { + turnContext.sendActivity(MessageFactory.text("activity 1")).join(); + turnContext.sendActivity(MessageFactory.text("activity 2")).join(); + turnContext.sendActivity(MessageFactory.text("activity 3")).join(); + return CompletableFuture.completedFuture(null); + }; + + Activity inboundActivity = new Activity(ActivityTypes.MESSAGE); + inboundActivity.setChannelId(Channels.EMULATOR); + inboundActivity.setServiceUrl("http://tempuri.org/whatever"); + inboundActivity.setText("hello world"); + inboundActivity.setConversation(new ConversationAccount("conversationId")); + + InvokeResponse invokeResponse = adapter.processActivity((String) null, inboundActivity, callback).join(); + + Assert.assertNull(invokeResponse); + Assert.assertEquals(3, ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateSetTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateSetTests.java new file mode 100644 index 000000000..16ac34da1 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateSetTests.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.junit.Assert; +import org.junit.Test; + +public class BotStateSetTests { + @Test + public void BotStateSet_Properties() { + Storage storage = new MemoryStorage(); + + UserState userState = new UserState(storage); + ConversationState conversationState = new ConversationState(storage); + BotStateSet stateSet = new BotStateSet(userState, conversationState); + + Assert.assertEquals(2, stateSet.getBotStates().size()); + Assert.assertTrue(stateSet.getBotStates().get(0) instanceof UserState); + Assert.assertTrue(stateSet.getBotStates().get(1) instanceof ConversationState); + } + + @Test + public void BotStateSet_LoadAsync() { + Storage storage = new MemoryStorage(); + + TurnContext turnContext = TestUtilities.createEmptyContext(); + + { + UserState userState = new UserState(storage); + StatePropertyAccessor userProperty = userState.createProperty("userCount"); + + ConversationState convState = new ConversationState(storage); + StatePropertyAccessor convProperty = convState.createProperty("convCount"); + + BotStateSet stateSet = new BotStateSet(userState, convState); + + Assert.assertEquals(2, stateSet.getBotStates().size()); + + Integer userCount = userProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(0, userCount.intValue()); + Integer convCount = convProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(0, convCount.intValue()); + + userProperty.set(turnContext, 10).join(); + convProperty.set(turnContext, 20).join(); + + stateSet.saveAllChanges(turnContext).join(); + } + + { + UserState userState = new UserState(storage); + StatePropertyAccessor userProperty = userState.createProperty("userCount"); + + ConversationState convState = new ConversationState(storage); + StatePropertyAccessor convProperty = convState.createProperty("convCount"); + + BotStateSet stateSet = new BotStateSet(userState, convState); + + stateSet.loadAll(turnContext).join(); + + Integer userCount = userProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(10, userCount.intValue()); + Integer convCount = convProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(20, convCount.intValue()); + } + } + + @Test + public void BotStateSet_SaveAsync() { + Storage storage = new MemoryStorage(); + + UserState userState = new UserState(storage); + StatePropertyAccessor userProperty = userState.createProperty("userCount"); + + ConversationState convState = new ConversationState(storage); + StatePropertyAccessor convProperty = convState.createProperty("convCount"); + + BotStateSet stateSet = new BotStateSet(userState, convState); + + Assert.assertEquals(2, stateSet.getBotStates().size()); + + TurnContext turnContext = TestUtilities.createEmptyContext(); + stateSet.loadAll(turnContext).join(); + + Integer userCount = userProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(0, userCount.intValue()); + Integer convCount = convProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(0, convCount.intValue()); + + userProperty.set(turnContext, 10).join(); + convProperty.set(turnContext, 20).join(); + + stateSet.saveAllChanges(turnContext).join(); + + userCount = userProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(10, userCount.intValue()); + convCount = convProperty.get(turnContext, () -> 0).join(); + Assert.assertEquals(20, convCount.intValue()); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTests.java new file mode 100644 index 000000000..a673b27a0 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotStateTests.java @@ -0,0 +1,953 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationAccount; +import java.util.concurrent.CompletionException; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class BotStateTests { + + @Test(expected = IllegalArgumentException.class) + public void State_EmptyName() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + + StatePropertyAccessor propertyA = userState.createProperty(""); + } + + @Test(expected = IllegalArgumentException.class) + public void State_NullName() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + + StatePropertyAccessor propertyA = userState.createProperty(null); + } + + @Test + public void MakeSureStorageNotCalledNoChangesAsync() { + int[] storeCount = { 0 }; + int[] readCount = { 0 }; + + Storage mock = new Storage() { + Map dictionary = new HashMap<>(); + + @Override + public CompletableFuture> read(String[] keys) { + readCount[0]++; + return CompletableFuture.completedFuture(dictionary); + } + + @Override + public CompletableFuture write(Map changes) { + storeCount[0]++; + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture delete(String[] keys) { + return CompletableFuture.completedFuture(null); + } + }; + + UserState userState = new UserState(mock); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("propertyA"); + Assert.assertEquals(storeCount[0], 0); + + userState.saveChanges(context).join(); + propertyA.set(context, "hello"); + Assert.assertEquals(1, readCount[0]); + Assert.assertEquals(0, storeCount[0]); + + propertyA.set(context, "there").join(); + Assert.assertEquals(0, storeCount[0]); // Set on property should not bump + + userState.saveChanges(context).join(); + Assert.assertEquals(1, storeCount[0]); // Explicit save should bump + + String valueA = propertyA.get(context, null).join(); + Assert.assertEquals("there", valueA); + Assert.assertEquals(1, storeCount[0]); // Gets should not bump + + userState.saveChanges(context).join(); + Assert.assertEquals(1, storeCount[0]); // Gets should not bump + + propertyA.delete(context).join(); + Assert.assertEquals(1, storeCount[0]); // Delete alone no bump + + userState.saveChanges(context).join(); + Assert.assertEquals(2, storeCount[0]); // Save when dirty should bump + Assert.assertEquals(1, readCount[0]); + + userState.saveChanges(context).join(); + Assert.assertEquals(2, storeCount[0]); // Save not dirty should not bump + Assert.assertEquals(1, readCount[0]); + } + + @Test + public void State_SetNoLoad() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("propertyA"); + propertyA.set(context, "hello").join(); + } + + @Test + public void State_MultipleLoads() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("propertyA"); + userState.load(context).join(); + userState.load(context).join(); + } + + @Test + public void State_GetNoLoadWithDefault() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("propertyA"); + String valueA = propertyA.get(context, () -> "Default!").join(); + Assert.assertEquals("Default!", valueA); + } + + @Test + public void State_GetNoLoadNoDefault() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("propertyA"); + String valueA = propertyA.get(context, null).join(); + + Assert.assertNull(valueA); + } + + @Test + public void State_POCO_NoDefault() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestPocoState value = testProperty.get(context, null).join(); + + Assert.assertNull(value); + } + + @Test + public void State_bool_NoDefault() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor testProperty = userState.createProperty("test"); + Boolean value = testProperty.get(context, null).join(); + + Assert.assertNull(value); + } + + @Test + public void State_int_NoDefault() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor testProperty = userState.createProperty("test"); + Integer value = testProperty.get(context).join(); + + Assert.assertNull(value); + } + + @Test + public void State_SetAfterSave() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("property-a"); + StatePropertyAccessor propertyB = userState.createProperty("property-b"); + + userState.load(context).join(); + propertyA.set(context, "hello").join(); + propertyB.set(context, "world").join(); + userState.saveChanges(context).join(); + + propertyA.set(context, "hello2").join(); + } + + @Test + public void State_MultipleSave() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("property-a"); + StatePropertyAccessor propertyB = userState.createProperty("property-b"); + + userState.load(context).join(); + propertyA.set(context, "hello").join(); + propertyB.set(context, "world").join(); + userState.saveChanges(context).join(); + + propertyA.set(context, "hello2").join(); + userState.saveChanges(context).join(); + + String valueA = propertyA.get(context).join(); + Assert.assertEquals("hello2", valueA); + } + + @Test + public void LoadSetSave() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("property-a"); + StatePropertyAccessor propertyB = userState.createProperty("property-b"); + + userState.load(context).join(); + propertyA.set(context, "hello").join(); + propertyB.set(context, "world").join(); + userState.saveChanges(context).join(); + + JsonNode obj = dictionary.get("EmptyContext/users/empty@empty.context.org"); + Assert.assertEquals("hello", obj.get("property-a").textValue()); + Assert.assertEquals("world", obj.get("property-b").textValue()); + } + + @Test + public void LoadSetSaveTwice() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + + StatePropertyAccessor propertyA = userState.createProperty("property-a"); + StatePropertyAccessor propertyB = userState.createProperty("property-b"); + StatePropertyAccessor propertyC = userState.createProperty("property-c"); + + userState.load(context).join(); + propertyA.set(context, "hello").join(); + propertyB.set(context, "world").join(); + propertyC.set(context, "test").join(); + userState.saveChanges(context).join(); + + JsonNode obj = dictionary.get("EmptyContext/users/empty@empty.context.org"); + Assert.assertEquals("hello", obj.get("property-a").textValue()); + Assert.assertEquals("world", obj.get("property-b").textValue()); + + // Act 2 + UserState userState2 = new UserState(new MemoryStorage(dictionary)); + + StatePropertyAccessor propertyA2 = userState.createProperty("property-a"); + StatePropertyAccessor propertyB2 = userState.createProperty("property-b"); + + userState.load(context).join(); + propertyA.set(context, "hello-2").join(); + propertyB.set(context, "world-2").join(); + userState2.saveChanges(context).join(); + + // Assert 2 + JsonNode obj2 = dictionary.get("EmptyContext/users/empty@empty.context.org"); + Assert.assertEquals("hello-2", obj2.get("property-a").textValue()); + Assert.assertEquals("world-2", obj2.get("property-b").textValue()); + Assert.assertEquals("test", obj2.get("property-c").textValue()); + } + + @Test + public void LoadSaveDelete() { + Map dictionary = new HashMap<>(); + TurnContext context = TestUtilities.createEmptyContext(); + + // Act + UserState userState = new UserState(new MemoryStorage(dictionary)); + + StatePropertyAccessor propertyA = userState.createProperty("property-a"); + StatePropertyAccessor propertyB = userState.createProperty("property-b"); + + userState.load(context).join(); + propertyA.set(context, "hello").join(); + propertyB.set(context, "world").join(); + userState.saveChanges(context).join(); + + // Assert + JsonNode obj = dictionary.get("EmptyContext/users/empty@empty.context.org"); + Assert.assertEquals("hello", obj.get("property-a").textValue()); + Assert.assertEquals("world", obj.get("property-b").textValue()); + + // Act 2 + UserState userState2 = new UserState(new MemoryStorage(dictionary)); + + StatePropertyAccessor propertyA2 = userState.createProperty("property-a"); + StatePropertyAccessor propertyB2 = userState.createProperty("property-b"); + + userState2.load(context).join(); + propertyA.set(context, "hello-2").join(); + propertyB.delete(context).join(); + userState2.saveChanges(context).join(); + + // Assert 2 + JsonNode obj2 = dictionary.get("EmptyContext/users/empty@empty.context.org"); + Assert.assertEquals("hello-2", obj2.get("property-a").textValue()); + Assert.assertNull(obj2.get("property-b")); + } + + @Test + public void State_DoNOTRememberContextState() { + TestAdapter adapter = new TestAdapter(); + + new TestFlow(adapter, (turnContext -> { + UserState obj = turnContext.getTurnState().get("UserState"); + Assert.assertNull(obj); + return CompletableFuture.completedFuture(null); + })).send("set value").startTest().join(); + } + + @Test + public void State_RememberIStoreItemUserState() { + UserState userState = new UserState(new MemoryStorage()); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestAdapter adapter = new TestAdapter().use(new AutoSaveStateMiddleware(userState)); + + BotCallbackHandler callback = (context) -> { + TestPocoState state = testProperty.get(context, TestPocoState::new).join(); + Assert.assertNotNull("user state should exist", state); + + switch (context.getActivity().getText()) { + case "set value": + state.setValue("test"); + context.sendActivity("value saved").join(); + break; + + case "get value": + context.sendActivity(state.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + }; + + new TestFlow(adapter, callback).test("set value", "value saved").test( + "get value", + "test" + ).startTest().join(); + } + + @Test + public void State_RememberPocoUserState() { + UserState userState = new UserState(new MemoryStorage()); + StatePropertyAccessor testPocoProperty = userState.createProperty( + "testPoco" + ); + TestAdapter adapter = new TestAdapter().use(new AutoSaveStateMiddleware(userState)); + + new TestFlow(adapter, (turnContext -> { + TestPocoState testPocoState = testPocoProperty.get( + turnContext, + TestPocoState::new + ).join(); + Assert.assertNotNull("user state should exist", testPocoState); + + switch (turnContext.getActivity().getText()) { + case "set value": + testPocoState.setValue("test"); + turnContext.sendActivity("value saved").join(); + break; + + case "get value": + turnContext.sendActivity(testPocoState.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + })).test("set value", "value saved").test("get value", "test").startTest().join(); + } + + @Test + public void State_RememberIStoreItemConversationState() { + ConversationState conversationState = new ConversationState(new MemoryStorage()); + StatePropertyAccessor testProperty = conversationState.createProperty("test"); + TestAdapter adapter = new TestAdapter().use(new AutoSaveStateMiddleware(conversationState)); + + new TestFlow(adapter, (turnContext -> { + TestState testState = testProperty.get(turnContext, TestState::new).join(); + Assert.assertNotNull("user state.conversation should exist", conversationState); + + switch (turnContext.getActivity().getText()) { + case "set value": + testState.setValue("test"); + turnContext.sendActivity("value saved").join(); + break; + + case "get value": + turnContext.sendActivity(testState.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + })).test("set value", "value saved").test("get value", "test").startTest().join(); + } + + @Test + public void State_RememberPocoConversationState() { + ConversationState conversationState = new ConversationState(new MemoryStorage()); + StatePropertyAccessor testProperty = conversationState.createProperty( + "testPoco" + ); + TestAdapter adapter = new TestAdapter().use(new AutoSaveStateMiddleware(conversationState)); + + new TestFlow(adapter, (turnContext -> { + TestPocoState testState = testProperty.get(turnContext, TestPocoState::new).join(); + Assert.assertNotNull("user state.conversation should exist", testState); + + switch (turnContext.getActivity().getText()) { + case "set value": + testState.setValue("test"); + turnContext.sendActivity("value saved").join(); + break; + + case "get value": + turnContext.sendActivity(testState.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + })).test("set value", "value saved").test("get value", "test").startTest().join(); + } + + @Test + public void State_RememberPocoPrivateConversationState() { + PrivateConversationState privateConversationState = new PrivateConversationState( + new MemoryStorage() + ); + StatePropertyAccessor testProperty = privateConversationState.createProperty( + "testPoco" + ); + TestAdapter adapter = new TestAdapter().use( + new AutoSaveStateMiddleware(privateConversationState) + ); + + new TestFlow(adapter, (turnContext -> { + TestPocoState testState = testProperty.get(turnContext, TestPocoState::new).join(); + Assert.assertNotNull("user state.conversation should exist", testState); + + switch (turnContext.getActivity().getText()) { + case "set value": + testState.setValue("test"); + turnContext.sendActivity("value saved").join(); + break; + + case "get value": + turnContext.sendActivity(testState.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + })).test("set value", "value saved").test("get value", "test").startTest().join(); + } + + @Test + public void State_CustomStateManagerTest() { + String testGuid = UUID.randomUUID().toString(); + CustomKeyState customState = new CustomKeyState(new MemoryStorage()); + + StatePropertyAccessor testProperty = customState.createProperty("test"); + + TestAdapter adapter = new TestAdapter().use(new AutoSaveStateMiddleware(customState)); + + new TestFlow(adapter, (turnContext -> { + TestPocoState testState = testProperty.get(turnContext, TestPocoState::new).join(); + Assert.assertNotNull("user state.conversation should exist", testState); + + switch (turnContext.getActivity().getText()) { + case "set value": + testState.setValue(testGuid); + turnContext.sendActivity("value saved").join(); + break; + + case "get value": + turnContext.sendActivity(testState.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + })).test("set value", "value saved").test("get value", testGuid).startTest().join(); + } + + @Test + public void State_RoundTripTypedObject() { + ConversationState convoState = new ConversationState(new MemoryStorage()); + StatePropertyAccessor testProperty = convoState.createProperty("typed"); + TestAdapter adapter = new TestAdapter().use(new AutoSaveStateMiddleware(convoState)); + + new TestFlow(adapter, (turnContext -> { + TypedObject testState = testProperty.get(turnContext, TypedObject::new).join(); + Assert.assertNotNull("conversationstate should exist"); + + switch (turnContext.getActivity().getText()) { + case "set value": + testState.setName("test"); + turnContext.sendActivity("value saved").join(); + break; + + case "get value": + turnContext.sendActivity(testState.getClass().getSimpleName()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + })).test("set value", "value saved").test("get value", "TypedObject").startTest().join(); + } + + @Test + public void State_UseBotStateDirectly() { + TestAdapter adapter = new TestAdapter(); + + new TestFlow(adapter, turnContext -> { + TestBotState botStateManager = new TestBotState(new MemoryStorage()); + StatePropertyAccessor testProperty = botStateManager.createProperty( + "test" + ); + + // read initial state object + botStateManager.load(turnContext).join(); + + CustomState customState = testProperty.get(turnContext, CustomState::new).join(); + + // this should be a 'new CustomState' as nothing is currently stored in storage + Assert.assertNotNull(customState); + Assert.assertTrue(customState.getCustomString() == null); + + customState.setCustomString("test"); + botStateManager.saveChanges(turnContext).join(); + + customState.setCustomString("asdfsadf"); + + // force read into context again (without save) + botStateManager.load(turnContext, true).join(); + + customState = testProperty.get(turnContext, CustomState::new).join(); + + // check object read from value has the correct value for CustomString + Assert.assertEquals("test", customState.getCustomString()); + + return CompletableFuture.completedFuture(null); + }).send(Activity.createConversationUpdateActivity()).startTest().join(); + } + + @Test(expected = CompletionException.class) + public void UserState_NullChannelIdThrows() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setChannelId(null); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void UserState_EmptyChannelIdThrows() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setChannelId(""); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void UserState_NullFromThrows() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setFrom(null); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void UserState_NullFromIdThrows() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getFrom().setId(null); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void UserState_EmptyFromIdThrows() { + Map dictionary = new HashMap<>(); + UserState userState = new UserState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getFrom().setId(""); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void ConversationState_NullConversationThrows() { + Map dictionary = new HashMap<>(); + ConversationState conversationState = new ConversationState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setConversation(null); + StatePropertyAccessor testProperty = conversationState.createProperty( + "test" + ); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void ConversationState_NullConversationIdThrows() { + Map dictionary = new HashMap<>(); + ConversationState conversationState = new ConversationState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getConversation().setId(null); + StatePropertyAccessor testProperty = conversationState.createProperty( + "test" + ); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void ConversationState_EmptyConversationIdThrows() { + Map dictionary = new HashMap<>(); + ConversationState conversationState = new ConversationState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getConversation().setId(""); + StatePropertyAccessor testProperty = conversationState.createProperty( + "test" + ); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void ConversationState_NullChannelIdThrows() { + Map dictionary = new HashMap<>(); + ConversationState conversationState = new ConversationState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setChannelId(null); + StatePropertyAccessor testProperty = conversationState.createProperty( + "test" + ); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void ConversationState_EmptyChannelIdThrows() { + Map dictionary = new HashMap<>(); + ConversationState conversationState = new ConversationState(new MemoryStorage(dictionary)); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setChannelId(""); + StatePropertyAccessor testProperty = conversationState.createProperty( + "test" + ); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_NullChannelIdThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setChannelId(null); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_EmptyChannelIdThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setChannelId(""); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_NullFromThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setFrom(null); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_NullFromIdThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getFrom().setId(null); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_EmptyFromIdThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getFrom().setId(""); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_NullConversationThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().setConversation(null); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_NullConversationIdThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getConversation().setId(null); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test(expected = CompletionException.class) + public void PrivateConversationState_EmptyConversationIdThrows() { + Map dictionary = new HashMap<>(); + PrivateConversationState botState = new PrivateConversationState( + new MemoryStorage(dictionary) + ); + TurnContext context = TestUtilities.createEmptyContext(); + context.getActivity().getConversation().setId(""); + StatePropertyAccessor testProperty = botState.createProperty("test"); + TestPocoState value = testProperty.get(context).join(); + } + + @Test + public void ClearAndSave() { + TurnContext turnContext = TestUtilities.createEmptyContext(); + turnContext.getActivity().setConversation(new ConversationAccount("1234")); + + Storage storage = new MemoryStorage(new HashMap<>()); + + // Turn 0 + ConversationState botState0 = new ConversationState(storage); + StatePropertyAccessor accessor0 = botState0.createProperty("test-name"); + TestPocoState value0 = accessor0.get( + turnContext, + () -> new TestPocoState("test-value") + ).join(); + value0.setValue("test-value"); + botState0.saveChanges(turnContext).join(); + + // Turn 1 + ConversationState botState1 = new ConversationState(storage); + StatePropertyAccessor accessor1 = botState1.createProperty("test-name"); + TestPocoState value1 = accessor1.get( + turnContext, + () -> new TestPocoState("default-value") + ).join(); + botState1.saveChanges(turnContext).join(); + + Assert.assertEquals("test-value", value1.getValue()); + + // Turn 2 + ConversationState botState3 = new ConversationState(storage); + botState3.clearState(turnContext).join(); + botState3.saveChanges(turnContext).join(); + + // Turn 3 + ConversationState botState4 = new ConversationState(storage); + StatePropertyAccessor accessor3 = botState4.createProperty("test-name"); + TestPocoState value4 = accessor1.get( + turnContext, + () -> new TestPocoState("default-value") + ).join(); + + Assert.assertEquals("default-value", value4.getValue()); + } + + @Test + public void BotStateDelete() { + TurnContext turnContext = TestUtilities.createEmptyContext(); + turnContext.getActivity().setConversation(new ConversationAccount("1234")); + + Storage storage = new MemoryStorage(new HashMap<>()); + + // Turn 0 + ConversationState botState0 = new ConversationState(storage); + StatePropertyAccessor accessor0 = botState0.createProperty("test-name"); + TestPocoState value0 = accessor0.get( + turnContext, + () -> new TestPocoState("test-value") + ).join(); + value0.setValue("test-value"); + botState0.saveChanges(turnContext).join(); + + // Turn 1 + ConversationState botState1 = new ConversationState(storage); + StatePropertyAccessor accessor1 = botState1.createProperty("test-name"); + TestPocoState value1 = accessor1.get( + turnContext, + () -> new TestPocoState("default-value") + ).join(); + botState1.saveChanges(turnContext).join(); + + Assert.assertEquals("test-value", value1.getValue()); + + // Turn 2 + ConversationState botState2 = new ConversationState(storage); + botState2.delete(turnContext).join(); + + // Turn 3 + ConversationState botState3 = new ConversationState(storage); + StatePropertyAccessor accessor3 = botState3.createProperty("test-name"); + TestPocoState value3 = accessor1.get( + turnContext, + () -> new TestPocoState("default-value") + ).join(); + botState1.saveChanges(turnContext).join(); + + Assert.assertEquals("default-value", value3.getValue()); + } + + private static class TestPocoState { + private String value; + + public TestPocoState() { + + } + + public TestPocoState(String withValue) { + value = withValue; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + private static class CustomState implements StoreItem { + private String _customString; + private String _eTag; + + public String getCustomString() { + return _customString; + } + + public void setCustomString(String customString) { + this._customString = customString; + } + + public String getETag() { + return _eTag; + } + + public void setETag(String eTag) { + this._eTag = eTag; + } + } + + private static class TypedObject { + @JsonProperty + private String name; + + public String name() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + } + + private static class TestState implements StoreItem { + private String etag; + private String value; + + @Override + public String getETag() { + return this.etag; + } + + @Override + public void setETag(String etag) { + this.etag = etag; + } + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + } + + private static class TestBotState extends BotState { + public TestBotState(Storage withStorage) { + super(withStorage, TestBotState.class.getSimpleName()); + } + + // "botstate/{turnContext.Activity.ChannelId}/{turnContext.Activity.Conversation.Id}/{typeof(BotState).Namespace}.{typeof(BotState).Name}"; + @Override + public String getStorageKey(TurnContext turnContext) { + return "botstate/" + turnContext.getActivity().getConversation().getId() + "/" + + BotState.class.getName(); + } + } + + private static class CustomKeyState extends BotState { + public static final String PROPERTY_NAME = "CustomKeyState"; + + public CustomKeyState(Storage storage) { + super(storage, PROPERTY_NAME); + } + + @Override + public String getStorageKey(TurnContext turnContext) { + return "CustomKey"; + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CallCountingMiddleware.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CallCountingMiddleware.java new file mode 100644 index 000000000..d7a5a2316 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CallCountingMiddleware.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +public class CallCountingMiddleware implements Middleware { + private int calls = 0; + + public int calls() { + return this.calls; + } + + public CallCountingMiddleware withCalls(int calls) { + this.calls = calls; + return this; + } + + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + this.calls++; + return next.next(); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CallOnException.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CallOnException.java new file mode 100644 index 000000000..9ba3bbcc0 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CallOnException.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +@FunctionalInterface +public interface CallOnException { + CompletableFuture invoke(TurnContext context, T t); +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchExceptionMiddleware.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchExceptionMiddleware.java new file mode 100644 index 000000000..5e5b2bdde --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CatchExceptionMiddleware.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This piece of middleware can be added to allow you to handle exceptions when + * they are thrown within your bot's code or middleware further down the + * pipeline. Using this handler you might send an appropriate message to the + * user to let them know that something has gone wrong. You can specify the type + * of exception the middleware should catch and this middleware can be added + * multiple times to allow you to handle different exception types in different + * ways. + */ +public class CatchExceptionMiddleware implements Middleware { + private CallOnException handler; + private Class exceptionType; + + public CatchExceptionMiddleware( + CallOnException withCallOnException, + Class withExceptionType + ) { + handler = withCallOnException; + exceptionType = withExceptionType; + } + + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + + Class c = exceptionType.getDeclaringClass(); + + // Continue to route the activity through the pipeline + // any errors further down the pipeline will be caught by + // this try / catch + return next.next().exceptionally(exception -> { + if (exceptionType.isInstance(exception)) { + handler.invoke(context, exception); + } else { + throw new CompletionException(exception); + } + + return null; + }); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ChannelServiceHandlerTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ChannelServiceHandlerTests.java new file mode 100644 index 000000000..beaf5bacf --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ChannelServiceHandlerTests.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ResourceResponse; + +import org.junit.Assert; +import org.junit.Test; + +public class ChannelServiceHandlerTests { + + @Test + public void AuthenticateSetsAnonymousSkillClaim() { + TestChannelServiceHandler sut = new TestChannelServiceHandler(); + sut.handleReplyToActivity(null, "123", "456", new Activity(ActivityTypes.MESSAGE)); + + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, + sut.getClaimsIdentity().getType()); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_SKILL_APPID, + JwtTokenValidation.getAppIdFromClaims(sut.getClaimsIdentity().claims())); + } + + @Test + public void testHandleSendToConversation() { + TestChannelServiceHandler sut = new TestChannelServiceHandler(); + sut.handleSendToConversation(null, "456", new Activity(ActivityTypes.MESSAGE)); + + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, + sut.getClaimsIdentity().getType()); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_SKILL_APPID, + JwtTokenValidation.getAppIdFromClaims(sut.getClaimsIdentity().claims())); + } + + + /** + * A {@link ChannelServiceHandler} with overrides for testings. + */ + private class TestChannelServiceHandler extends ChannelServiceHandler { + TestChannelServiceHandler() { + super(new SimpleCredentialProvider(), new AuthenticationConfiguration(), null); + } + + private ClaimsIdentity claimsIdentity; + + @Override + protected CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity + ) { + this.claimsIdentity = claimsIdentity; + return CompletableFuture.completedFuture(new ResourceResponse()); + } + + @Override + protected CompletableFuture onSendToConversation( + ClaimsIdentity claimsIdentity, + String activityId, + Activity activity + ) { + this.claimsIdentity = claimsIdentity; + return CompletableFuture.completedFuture(new ResourceResponse()); + } + + /** + * Gets the {@link ClaimsIdentity} sent to the different methods after + * auth is done. + * @return the ClaimsIdentity value as a getClaimsIdentity(). + */ + public ClaimsIdentity getClaimsIdentity() { + return this.claimsIdentity; + } + + /** + * Gets the {@link ClaimsIdentity} sent to the different methods after + * auth is done. + * @param withClaimsIdentity The ClaimsIdentity value. + */ + private void setClaimsIdentity(ClaimsIdentity withClaimsIdentity) { + this.claimsIdentity = withClaimsIdentity; + } + + } +} + diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/EventFactoryTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/EventFactoryTests.java new file mode 100644 index 000000000..7a5cd1acc --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/EventFactoryTests.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.HandoffEventNames; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.Transcript; + +import org.junit.Assert; +import org.junit.Test; + +public class EventFactoryTests { + + @Test + public void HandoffInitiationNullTurnContext() { + Assert.assertThrows(IllegalArgumentException.class, + () -> EventFactory.createHandoffInitiation(null, "some text")); + } + + @Test + public void HandoffStatusNullConversation() { + Assert.assertThrows(IllegalArgumentException.class, () -> EventFactory.createHandoffStatus(null, "accepted")); + } + + @Test + public void HandoffStatusNullStatus() { + Assert.assertThrows(IllegalArgumentException.class, + () -> EventFactory.createHandoffStatus(new ConversationAccount(), null)); + } + + @Test + public void TestCreateHandoffInitiation() { + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("TestCreateHandoffInitiation", "User1", "Bot")); + String fromD = "test"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(""); + activity.setConversation(new ConversationAccount()); + activity.setRecipient(new ChannelAccount()); + activity.setFrom(new ChannelAccount(fromD)); + activity.setChannelId("testchannel"); + activity.setServiceUrl("http://myservice"); + TurnContext context = new TurnContextImpl(adapter, activity); + List activities = new ArrayList(); + activities.add(MessageFactory.text("hello")); + Transcript transcript = new Transcript(); + transcript.setActivities(activities); + + Assert.assertNull(transcript.getActivities().get(0).getChannelId()); + Assert.assertNull(transcript.getActivities().get(0).getServiceUrl()); + Assert.assertNull(transcript.getActivities().get(0).getConversation()); + + ObjectNode handoffContext = JsonNodeFactory.instance.objectNode(); + handoffContext.set("Skill", JsonNodeFactory.instance.textNode("any")); + + Activity handoffEvent = EventFactory.createHandoffInitiation(context, handoffContext, transcript); + Assert.assertEquals(handoffEvent.getName(), HandoffEventNames.INITIATEHANDOFF); + ObjectNode node = (ObjectNode) handoffEvent.getValue(); + String skill = node.get("Skill").asText(); + Assert.assertEquals("any", skill); + Assert.assertEquals(handoffEvent.getFrom().getId(), fromD); + } + + @Test + public void TestCreateHandoffInitiationNoTranscript() { + TestAdapter adapter = new TestAdapter( + TestAdapter.createConversationReference("TestCreateHandoffInitiation", "User1", "Bot")); + String fromD = "test"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(""); + activity.setConversation(new ConversationAccount()); + activity.setRecipient(new ChannelAccount()); + activity.setFrom(new ChannelAccount(fromD)); + activity.setChannelId("testchannel"); + activity.setServiceUrl("http://myservice"); + TurnContext context = new TurnContextImpl(adapter, activity); + List activities = new ArrayList(); + activities.add(MessageFactory.text("hello")); + + ObjectNode handoffContext = JsonNodeFactory.instance.objectNode(); + handoffContext.set("Skill", JsonNodeFactory.instance.textNode("any")); + + Activity handoffEvent = EventFactory.createHandoffInitiation(context, handoffContext); + Assert.assertEquals(handoffEvent.getName(), HandoffEventNames.INITIATEHANDOFF); + ObjectNode node = (ObjectNode) handoffEvent.getValue(); + String skill = node.get("Skill").asText(); + Assert.assertEquals("any", skill); + Assert.assertEquals(handoffEvent.getFrom().getId(), fromD); + } + + @Test + public void TestCreateHandoffStatus() throws JsonProcessingException { + String state = "failed"; + String message = "timed out"; + Activity handoffEvent = EventFactory.createHandoffStatus(new ConversationAccount(), state, message); + Assert.assertEquals(handoffEvent.getName(), HandoffEventNames.HANDOFFSTATUS); + + ObjectNode node = (ObjectNode) handoffEvent.getValue(); + + String stateFormEvent = node.get("state").asText(); + Assert.assertEquals(stateFormEvent, state); + + String messageFormEvent = node.get("message").asText(); + Assert.assertEquals(messageFormEvent, message); + + String status = Serialization.toString(node); + Assert.assertEquals(status, String.format("{\"state\":\"%s\",\"message\":\"%s\"}", state, message)); + Assert.assertNotNull(handoffEvent.getAttachments()); + Assert.assertNotNull(handoffEvent.getId()); + } + + @Test + public void TestCreateHandoffStatusNoMessage() { + String state = "failed"; + Activity handoffEvent = EventFactory.createHandoffStatus(new ConversationAccount(), state); + + ObjectNode node = (ObjectNode) handoffEvent.getValue(); + + String stateFormEvent = node.get("state").asText(); + Assert.assertEquals(stateFormEvent, state); + + JsonNode messageFormEvent = node.get("message"); + Assert.assertNull(messageFormEvent); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/InspectionTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/InspectionTests.java new file mode 100644 index 000000000..947ff831f --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/InspectionTests.java @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.inspection.InspectionMiddleware; +import com.microsoft.bot.builder.inspection.InspectionSession; +import com.microsoft.bot.builder.inspection.InspectionState; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.Entity; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class InspectionTests { + @Test + public void ScenarioWithInspectionMiddlewarePassthrough() { + InspectionState inspectionState = new InspectionState(new MemoryStorage()); + InspectionMiddleware inspectionMiddleware = new InspectionMiddleware(inspectionState); + + TestAdapter adapter = new TestAdapter().use(inspectionMiddleware); + + Activity inboundActivity = MessageFactory.text("hello"); + + adapter.processActivity(inboundActivity, turnContext -> { + turnContext.sendActivity(MessageFactory.text("hi")).join(); + return CompletableFuture.completedFuture(null); + }).join(); + + Activity outboundActivity = adapter.activeQueue().poll(); + Assert.assertEquals("hi", outboundActivity.getText()); + } + + @Test + public void ScenarioWithInspectionMiddlewareOpenAttach() throws IOException { + // any bot state should be returned as trace messages per turn + MemoryStorage storage = new MemoryStorage(); + InspectionState inspectionState = new InspectionState(storage); + UserState userState = new UserState(storage); + ConversationState conversationState = new ConversationState(storage); + + TestInspectionMiddleware inspectionMiddleware = new TestInspectionMiddleware( + inspectionState, + userState, + conversationState, + null + ); + + // (1) send the /INSPECT open command from the emulator to the middleware + Activity openActivity = MessageFactory.text("/INSPECT open"); + + TestAdapter inspectionAdapter = new TestAdapter(Channels.TEST, true); + inspectionAdapter.processActivity(openActivity, turnContext -> { + inspectionMiddleware.processCommand(turnContext).join(); + return CompletableFuture.completedFuture(null); + }).join(); + + Activity inspectionOpenResultActivity = inspectionAdapter.activeQueue().poll(); + + // (2) send the resulting /INSPECT attach command from the channel to the + // middleware + TestAdapter applicationAdapter = new TestAdapter(Channels.TEST); + applicationAdapter.use(inspectionMiddleware); + + String attachCommand = inspectionOpenResultActivity.getValue().toString(); + + applicationAdapter.processActivity( + MessageFactory.text(attachCommand), + turnContext -> { + // nothing happens - just attach the inspector + return CompletableFuture.completedFuture(null); + } + ).join(); + + Activity attachResponse = applicationAdapter.activeQueue().poll(); + + // (3) send an application messaage from the channel, it should get the reply + // and then so should the emulator http endpioint + applicationAdapter.processActivity(MessageFactory.text("hi"), turnContext -> { + turnContext.sendActivity( + MessageFactory.text("echo: " + turnContext.getActivity().getText()) + ).join(); + + userState.createProperty("x").get( + turnContext, + Scratch::new + ).join().setProperty("hello"); + conversationState.createProperty("y").get( + turnContext, + Scratch::new + ).join().setProperty("world"); + + userState.saveChanges(turnContext).join(); + conversationState.saveChanges(turnContext).join(); + + return CompletableFuture.completedFuture(null); + }).join(); + + Activity outboundActivity = applicationAdapter.activeQueue().poll(); + Assert.assertEquals("echo: hi", outboundActivity.getText()); + Assert.assertEquals(3, inspectionMiddleware.recordingSession.requests.size()); + + ObjectMapper mapper = new ObjectMapper(); + mapper.findAndRegisterModules(); + + JsonNode inboundTrace = mapper.readTree( + inspectionMiddleware.recordingSession.requests.get(0) + ); + Assert.assertEquals("trace", inboundTrace.get("type").textValue()); + Assert.assertEquals("ReceivedActivity", inboundTrace.get("name").textValue()); + Assert.assertEquals("message", inboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("hi", inboundTrace.get("value").get("text").textValue()); + + JsonNode outboundTrace = mapper.readTree( + inspectionMiddleware.recordingSession.requests.get(1) + ); + Assert.assertEquals("trace", outboundTrace.get("type").textValue()); + Assert.assertEquals("SentActivity", outboundTrace.get("name").textValue()); + Assert.assertEquals("message", outboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("echo: hi", outboundTrace.get("value").get("text").textValue()); + + JsonNode stateTrace = mapper.readTree( + inspectionMiddleware.recordingSession.requests.get(2) + ); + Assert.assertEquals("trace", stateTrace.get("type").textValue()); + Assert.assertEquals("BotState", stateTrace.get("name").textValue()); + Assert.assertEquals( + "hello", + stateTrace.get("value").get("userState").get("x").get("property").textValue() + ); + Assert.assertEquals( + "world", + stateTrace.get("value").get("conversationState").get("y").get("property").textValue() + ); + } + + @Test + public void ScenarioWithInspectionMiddlewareOpenAttachWithMention() throws IOException { + // any bot state should be returned as trace messages per turn + MemoryStorage storage = new MemoryStorage(); + InspectionState inspectionState = new InspectionState(storage); + UserState userState = new UserState(storage); + ConversationState conversationState = new ConversationState(storage); + + TestInspectionMiddleware inspectionMiddleware = new TestInspectionMiddleware( + inspectionState, + userState, + conversationState, + null + ); + + // (1) send the /INSPECT open command from the emulator to the middleware + Activity openActivity = MessageFactory.text("/INSPECT open"); + + TestAdapter inspectionAdapter = new TestAdapter(Channels.TEST, true); + inspectionAdapter.processActivity(openActivity, turnContext -> { + inspectionMiddleware.processCommand(turnContext).join(); + return CompletableFuture.completedFuture(null); + }).join(); + + Activity inspectionOpenResultActivity = inspectionAdapter.activeQueue().poll(); + + // (2) send the resulting /INSPECT attach command from the channel to the + // middleware + TestAdapter applicationAdapter = new TestAdapter(Channels.TEST); + applicationAdapter.use(inspectionMiddleware); + + // some channels - for example Microsoft Teams - adds an @ mention to the text - + // this should be taken into account when evaluating the INSPECT + String recipientId = "bot"; + String attachCommand = "" + recipientId + " " + + inspectionOpenResultActivity.getValue(); + Activity attachActivity = MessageFactory.text(attachCommand); + Entity entity = new Entity(); + entity.setType("mention"); + entity.getProperties().put("text", JsonNodeFactory.instance.textNode("" + recipientId + "")); + entity.getProperties().put("mentioned", JsonNodeFactory.instance.objectNode().put("id", "bot")); + attachActivity.getEntities().add(entity); + + applicationAdapter.processActivity( + attachActivity, + turnContext -> { + // nothing happens - just attach the inspector + return CompletableFuture.completedFuture(null); + } + ).join(); + + Activity attachResponse = applicationAdapter.activeQueue().poll(); + + // (3) send an application messaage from the channel, it should get the reply + // and then so should the emulator http endpioint + applicationAdapter.processActivity(MessageFactory.text("hi"), turnContext -> { + turnContext.sendActivity( + MessageFactory.text("echo: " + turnContext.getActivity().getText()) + ).join(); + + userState.createProperty("x").get( + turnContext, + Scratch::new + ).join().setProperty("hello"); + conversationState.createProperty("y").get( + turnContext, + Scratch::new + ).join().setProperty("world"); + + userState.saveChanges(turnContext).join(); + conversationState.saveChanges(turnContext).join(); + + return CompletableFuture.completedFuture(null); + }).join(); + + Activity outboundActivity = applicationAdapter.activeQueue().poll(); + Assert.assertEquals("echo: hi", outboundActivity.getText()); + Assert.assertEquals(3, inspectionMiddleware.recordingSession.requests.size()); + + ObjectMapper mapper = new ObjectMapper(); + mapper.findAndRegisterModules(); + + JsonNode inboundTrace = mapper.readTree( + inspectionMiddleware.recordingSession.requests.get(0) + ); + Assert.assertEquals("trace", inboundTrace.get("type").textValue()); + Assert.assertEquals("ReceivedActivity", inboundTrace.get("name").textValue()); + Assert.assertEquals("message", inboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("hi", inboundTrace.get("value").get("text").textValue()); + + JsonNode outboundTrace = mapper.readTree( + inspectionMiddleware.recordingSession.requests.get(1) + ); + Assert.assertEquals("trace", outboundTrace.get("type").textValue()); + Assert.assertEquals("SentActivity", outboundTrace.get("name").textValue()); + Assert.assertEquals("message", outboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("echo: hi", outboundTrace.get("value").get("text").textValue()); + + JsonNode stateTrace = mapper.readTree( + inspectionMiddleware.recordingSession.requests.get(2) + ); + Assert.assertEquals("trace", stateTrace.get("type").textValue()); + Assert.assertEquals("BotState", stateTrace.get("name").textValue()); + Assert.assertEquals( + "hello", + stateTrace.get("value").get("userState").get("x").get("property").textValue() + ); + Assert.assertEquals( + "world", + stateTrace.get("value").get("conversationState").get("y").get("property").textValue() + ); + } + + // We can't currently supply a custom httpclient like dotnet. So instead, these + // test differ from dotnet by + // supplying a custom InspectionSession that records what is sent through it. + private static class TestInspectionMiddleware extends InspectionMiddleware { + public RecordingInspectionSession recordingSession = null; + + public TestInspectionMiddleware(InspectionState withInspectionState) { + super(withInspectionState); + } + + public TestInspectionMiddleware( + InspectionState withInspectionState, + UserState withUserState, + ConversationState withConversationState, + MicrosoftAppCredentials withCredentials + ) { + super(withInspectionState, withUserState, withConversationState, withCredentials); + } + + @Override + protected InspectionSession createSession( + ConversationReference reference, + MicrosoftAppCredentials credentials + ) { + if (recordingSession == null) { + recordingSession = new RecordingInspectionSession(reference, credentials); + } + return recordingSession; + } + } + + private static class RecordingInspectionSession extends InspectionSession { + private List requests = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + + public RecordingInspectionSession( + ConversationReference withConversationReference, + MicrosoftAppCredentials withCredentials + ) { + super(withConversationReference, withCredentials); + mapper.findAndRegisterModules(); + } + + public RecordingInspectionSession( + ConversationReference withConversationReference, + MicrosoftAppCredentials withCredentials, + Logger withLogger + ) { + super(withConversationReference, withCredentials, withLogger); + mapper.findAndRegisterModules(); + } + + public List getRequests() { + return requests; + } + + @Override + public CompletableFuture send(Activity activity) { + try { + requests.add(mapper.writeValueAsString(activity)); + } catch (Throwable t) { + // noop + } + + return CompletableFuture.completedFuture(true); + } + } + + private static class Scratch { + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + + private String property; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java new file mode 100644 index 000000000..809dab22c --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Attachments; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; + +public class MemoryConnectorClient implements ConnectorClient { + private MemoryConversations conversations = new MemoryConversations(); + + @Override + public RestClient getRestClient() { + return null; + } + + @Override + public String baseUrl() { + return null; + } + + @Override + public ServiceClientCredentials credentials() { + return null; + } + + @Override + public String getUserAgent() { + return null; + } + + @Override + public String getAcceptLanguage() { + return null; + } + + @Override + public void setAcceptLanguage(String acceptLanguage) { + + } + + @Override + public int getLongRunningOperationRetryTimeout() { + return 0; + } + + @Override + public void setLongRunningOperationRetryTimeout(int timeout) { + + } + + @Override + public boolean getGenerateClientRequestId() { + return false; + } + + @Override + public void setGenerateClientRequestId(boolean generateClientRequestId) { + + } + + @Override + public Attachments getAttachments() { + return null; + } + + @Override + public Conversations getConversations() { + return conversations; + } + + @Override + public void close() throws Exception { + + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java new file mode 100644 index 000000000..aa21614cb --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.AttachmentData; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.PagedMembersResult; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Transcript; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.NotImplementedException; + +public class MemoryConversations implements Conversations { + private List sentActivities = new ArrayList<>(); + + public List getSentActivities() { + return sentActivities; + } + + @Override + public CompletableFuture getConversations() { + return notImplemented("getConversations"); + } + + @Override + public CompletableFuture getConversations(String continuationToken) { + return notImplemented("getConversations"); + } + + @Override + public CompletableFuture createConversation( + ConversationParameters parameters + ) { + return notImplemented("createConversation"); + } + + @Override + public CompletableFuture sendToConversation( + String conversationId, + Activity activity + ) { + sentActivities.add(activity); + ResourceResponse response = new ResourceResponse(); + response.setId(activity.getId()); + return CompletableFuture.completedFuture(response); + } + + @Override + public CompletableFuture updateActivity( + String conversationId, String activityId, + Activity activity + ) { + return notImplemented("updateActivity"); + } + + @Override + public CompletableFuture replyToActivity( + String conversationId, String activityId, + Activity activity + ) { + sentActivities.add(activity); + ResourceResponse response = new ResourceResponse(); + response.setId(activity.getId()); + return CompletableFuture.completedFuture(response); + } + + @Override + public CompletableFuture deleteActivity(String conversationId, String activityId) { + return notImplemented("deleteActivity"); + } + + @Override + public CompletableFuture> getConversationMembers( + String conversationId + ) { + return notImplemented("getConversationMembers"); + } + + @Override + public CompletableFuture getConversationMember( + String userId, String conversationId + ) { + return notImplemented("getConversationMember"); + } + + @Override + public CompletableFuture deleteConversationMember( + String conversationId, String memberId + ) { + return notImplemented("deleteConversationMember"); + } + + @Override + public CompletableFuture> getActivityMembers( + String conversationId, String activityId + ) { + return notImplemented("getActivityMembers"); + } + + @Override + public CompletableFuture uploadAttachment( + String conversationId, AttachmentData attachmentUpload + ) { + return notImplemented("uploadAttachment"); + } + + @Override + public CompletableFuture sendConversationHistory( + String conversationId, Transcript history + ) { + return notImplemented("sendConversationHistory"); + } + + @Override + public CompletableFuture getConversationPagedMembers( + String conversationId + ) { + return notImplemented("getConversationPagedMembers"); + } + + @Override + public CompletableFuture getConversationPagedMembers( + String conversationId, + String continuationToken + ) { + return notImplemented("getConversationPagedMembers"); + } + + protected CompletableFuture notImplemented(String message) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally( + new NotImplementedException(message) + ); + return result; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryStorageTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryStorageTests.java new file mode 100644 index 000000000..14cef849b --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryStorageTests.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.junit.Before; +import org.junit.Test; + +public class MemoryStorageTests extends StorageBaseTests { + private Storage storage; + + @Before + public void initialize() { + storage = new MemoryStorage(); + } + + @Test + public void MemoryStorage_CreateObjectTest() { + createObjectTest(storage); + } + + @Test + public void MemoryStorage_ReadUnknownTest() { + readUnknownTest(storage); + } + + @Test + public void MemoryStorage_UpdateObjectTest() { + updateObjectTest(storage); + } + + @Test + public void MemoryStorage_DeleteObjectTest() { + deleteObjectTest(storage); + } + + @Test + public void MemoryStorage_HandleCrazyKeys() { + handleCrazyKeys(storage); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryTranscriptTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryTranscriptTests.java new file mode 100644 index 000000000..f8b64a8db --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryTranscriptTests.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.junit.Test; + +public class MemoryTranscriptTests extends TranscriptBaseTests { + public MemoryTranscriptTests() { + store = new MemoryTranscriptStore(); + } + + @Test + public void MemoryTranscript_BadArgs() { + super.BadArgs(); + } + + @Test + public void MemoryTranscript_LogActivity() { + super.LogActivity(); + } + + @Test + public void MemoryTranscript_LogMultipleActivities() { + super.LogMultipleActivities(); + } + + @Test + public void MemoryTranscript_GetConversationActivities() { + super.GetTranscriptActivities(); + } + + @Test + public void MemoryTranscript_GetConversationActivitiesStartDate() { + super.GetTranscriptActivitiesStartDate(); + } + + @Test + public void MemoryTranscript_ListConversations() { + super.ListTranscripts(); + } + + @Test + public void MemoryTranscript_DeleteConversation() { + super.DeleteTranscript(); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MentionTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MentionTests.java new file mode 100644 index 000000000..17ff38043 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MentionTests.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Entity; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +public class MentionTests { + static ObjectMapper mapper = new ObjectMapper(); + static { + mapper.findAndRegisterModules(); + } + + @Test + public void Mention_Skype() throws IOException { + // A Skype mention contains the user mention enclosed in tags. But the + // activity.getText() (as below) + // does not. + String mentionJson = "{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}"; + Entity mention = mapper.readValue(mentionJson, Entity.class); + mention.setType("mention"); + + Activity activity = MessageFactory.text("botname sometext"); + activity.setChannelId("skype"); + activity.getEntities().add(mention); + + // Normalize the Skype mention so that it is in a format RemoveMentionText can + // handle. + // If SkypeMentionNormalizeMiddleware is added to the adapters Middleware set, + // this + // will be called on every Skype message. + SkypeMentionNormalizeMiddleware.normalizeSkypeMentionText(activity); + + // This will remove the Mention.Text from the activity.getText(). This should + // just leave before/after the + // mention. + activity.removeMentionText("recipientid"); + + Assert.assertEquals(activity.getText(), "sometext"); + } + + @Test + public void Mention_Teams() throws IOException { + String mentionJson = "{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}"; + Entity mention = mapper.readValue(mentionJson, Entity.class); + mention.setType("mention"); + + Activity activity = MessageFactory.text("botname sometext"); + activity.getEntities().add(mention); + + activity.removeMentionText("recipientid"); + + Assert.assertEquals(activity.getText(), "sometext"); + } + + @Test + public void Mention_slack() throws IOException { + String mentionJson = "{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"@botname\"}"; + Entity mention = mapper.readValue(mentionJson, Entity.class); + mention.setType("mention"); + + Activity activity = MessageFactory.text("@botname sometext"); + activity.getEntities().add(mention); + + activity.removeMentionText("recipientid"); + + Assert.assertEquals(activity.getText(), "sometext"); + } + + @Test + public void Mention_GroupMe() throws IOException { + String mentionJson = "{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"@bot name\"}"; + Entity mention = mapper.readValue(mentionJson, Entity.class); + mention.setType("mention"); + + Activity activity = MessageFactory.text("@bot name sometext"); + activity.getEntities().add(mention); + + activity.removeMentionText("recipientid"); + + Assert.assertEquals(activity.getText(), "sometext"); + } + + @Test + public void Mention_Telegram() throws IOException { + String mentionJson = "{\"mentioned\": {\"id\": \"recipientid\"},\"text\": \"botname\"}"; + Entity mention = mapper.readValue(mentionJson, Entity.class); + mention.setType("mention"); + + Activity activity = MessageFactory.text("botname sometext"); + activity.getEntities().add(mention); + + activity.removeMentionText("recipientid"); + + Assert.assertEquals(activity.getText(), "sometext"); + } + + @Test + public void Mention_Facebook() { + // no-op for now: Facebook mentions unknown at this time + } + + @Test + public void Mention_Email() { + // no-op for now: EMail mentions not included in activity.getText()? + } + + @Test + public void Mention_Cortana() { + // no-op for now: Cortana mentions unknown at this time + } + + @Test + public void Mention_Kik() { + // no-op for now: bot mentions in Kik don't get Entity info and not included in + // activity.getText() + } + + @Test + public void Mention_Twilio() { + // no-op for now: Twilio mentions unknown at this time. Could not determine if + // they are supported. + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MessageFactoryTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MessageFactoryTests.java new file mode 100644 index 000000000..5998af11f --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MessageFactoryTests.java @@ -0,0 +1,543 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.AttachmentLayoutTypes; +import com.microsoft.bot.schema.CardAction; +import com.microsoft.bot.schema.InputHints; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class MessageFactoryTests { + @Test + public void NullText() { + Activity message = MessageFactory.text(null); + Assert.assertNull( + "Message Text is not null. Null must have been passed through.", + message.getText() + ); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + } + + @Test + public void TextOnly() { + String messageText = UUID.randomUUID().toString(); + Activity message = MessageFactory.text(messageText); + Assert.assertEquals("Message Text does not match", messageText, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + } + + @Test + public void TextAndSSML() { + String messageText = UUID.randomUUID().toString(); + String ssml = "

Bots are Awesome.

"; + Activity message = MessageFactory.text(messageText, ssml, null); + Assert.assertEquals("Message Text is not an empty String", messageText, message.getText()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertEquals( + "InputHint is not AcceptingInput", + InputHints.ACCEPTING_INPUT, + message.getInputHint() + ); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + } + + @Test + public void SuggestedActionText() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + List textActions = Arrays.asList("one", "two"); + + Activity message = MessageFactory.suggestedActions(textActions, text, ssml, inputHint); + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertNotNull(message.getSuggestedActions()); + Assert.assertNotNull(message.getSuggestedActions().getActions()); + Assert.assertTrue(message.getSuggestedActions().getActions().size() == 2); + Assert.assertEquals("one", message.getSuggestedActions().getActions().get(0).getValue()); + Assert.assertEquals("one", message.getSuggestedActions().getActions().get(0).getTitle()); + Assert.assertEquals( + message.getSuggestedActions().getActions().get(0).getType(), + ActionTypes.IM_BACK + ); + Assert.assertEquals("two", message.getSuggestedActions().getActions().get(1).getValue()); + Assert.assertEquals("two", message.getSuggestedActions().getActions().get(1).getTitle()); + Assert.assertTrue( + message.getSuggestedActions().getActions().get(1).getType() == ActionTypes.IM_BACK + ); + } + + @Test + public void SuggestedActionEnumerable() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + Set textActions = new HashSet<>(Arrays.asList("one", "two", "three")); + + Activity message = MessageFactory.suggestedActions( + new ArrayList<>(textActions), + text, + ssml, + inputHint + ); + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertNotNull(message.getSuggestedActions()); + Assert.assertNotNull(message.getSuggestedActions().getActions()); + Assert.assertTrue( + "The message's suggested actions have the wrong set of values.", + textActions.containsAll( + message.getSuggestedActions().getActions().stream().map(CardAction::getValue).collect(Collectors.toList()) + ) + ); + Assert.assertTrue( + "The message's suggested actions have the wrong set of titles.", + textActions.containsAll( + message.getSuggestedActions().getActions().stream().map(CardAction::getTitle).collect(Collectors.toList()) + ) + ); + Assert.assertTrue( + "The message's suggested actions are of the wrong action type.", + message.getSuggestedActions().getActions().stream().allMatch( + action -> action.getType() == ActionTypes.IM_BACK + ) + ); + } + + @Test + public void SuggestedActionCardAction() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String cardActionValue = UUID.randomUUID().toString(); + String cardActionTitle = UUID.randomUUID().toString(); + + CardAction ca = new CardAction(); + ca.setType(ActionTypes.IM_BACK); + ca.setValue(cardActionValue); + ca.setTitle(cardActionTitle); + + List cardActions = Collections.singletonList(ca); + + Activity message = MessageFactory.suggestedCardActions(cardActions, text, ssml, inputHint); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertNotNull(message.getSuggestedActions()); + Assert.assertNotNull(message.getSuggestedActions().getActions()); + Assert.assertTrue(message.getSuggestedActions().getActions().size() == 1); + Assert.assertEquals( + cardActionValue, + message.getSuggestedActions().getActions().get(0).getValue() + ); + Assert.assertEquals( + cardActionTitle, + message.getSuggestedActions().getActions().get(0).getTitle() + ); + Assert.assertTrue( + message.getSuggestedActions().getActions().get(0).getType() == ActionTypes.IM_BACK + ); + } + + @Test + public void SuggestedActionCardActionUnordered() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String cardValue1 = UUID.randomUUID().toString(); + String cardTitle1 = UUID.randomUUID().toString(); + + CardAction cardAction1 = new CardAction(); + cardAction1.setType(ActionTypes.IM_BACK); + cardAction1.setValue(cardValue1); + cardAction1.setTitle(cardTitle1); + + String cardValue2 = UUID.randomUUID().toString(); + String cardTitle2 = UUID.randomUUID().toString(); + + CardAction cardAction2 = new CardAction(); + cardAction2.setType(ActionTypes.IM_BACK); + cardAction2.setValue(cardValue2); + cardAction2.setTitle(cardTitle2); + + List cardActions = Arrays.asList(cardAction1, cardAction2); + Set values = new HashSet<>(Arrays.asList(cardValue1, cardValue2)); + Set titles = new HashSet<>(Arrays.asList(cardTitle1, cardTitle2)); + + Activity message = MessageFactory.suggestedCardActions(cardActions, text, ssml, inputHint); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertNotNull(message.getSuggestedActions()); + Assert.assertNotNull(message.getSuggestedActions().getActions()); + Assert.assertTrue(message.getSuggestedActions().getActions().size() == 2); + Assert.assertTrue( + "The message's suggested actions have the wrong set of values.", + values.containsAll( + message.getSuggestedActions().getActions().stream().map(CardAction::getValue).collect(Collectors.toList()) + ) + ); + Assert.assertTrue( + "The message's suggested actions have the wrong set of titles.", + titles.containsAll( + message.getSuggestedActions().getActions().stream().map(CardAction::getTitle).collect(Collectors.toList()) + ) + ); + Assert.assertTrue( + "The message's suggested actions are of the wrong action type.", + message.getSuggestedActions().getActions().stream().allMatch( + action -> action.getType() == ActionTypes.IM_BACK + ) + ); + + } + + @Test + public void AttachmentSingle() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String attachmentName = UUID.randomUUID().toString(); + Attachment a = new Attachment(); + a.setName(attachmentName); + + Activity message = MessageFactory.attachment(a, text, ssml, inputHint); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertTrue("Incorrect Attachment Count", message.getAttachments().size() == 1); + Assert.assertEquals( + "Incorrect Attachment Name", + message.getAttachments().get(0).getName(), + attachmentName + ); + } + + @Test(expected = IllegalArgumentException.class) + public void AttachmentNull() { + Activity message = MessageFactory.attachment(null, null); + Assert.fail("Exception not thrown"); + } + + @Test(expected = IllegalArgumentException.class) + public void AttachmentMultipleNull() { + Activity message = MessageFactory.attachment((List) null, null, null, null); + Assert.fail("Exception not thrown"); + } + + @Test(expected = IllegalArgumentException.class) + public void CarouselNull() { + Activity message = MessageFactory.carousel(null, null); + Assert.fail("Exception not thrown"); + } + + @Test + public void CarouselTwoAttachments() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String attachmentName = UUID.randomUUID().toString(); + Attachment attachment1 = new Attachment(); + attachment1.setName(attachmentName); + + String attachmentName2 = UUID.randomUUID().toString(); + Attachment attachment2 = new Attachment(); + attachment2.setName(attachmentName2); + + List multipleAttachments = Arrays.asList(attachment1, attachment2); + Activity message = MessageFactory.carousel(multipleAttachments, text, ssml, inputHint); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertTrue(message.getAttachmentLayout() == AttachmentLayoutTypes.CAROUSEL); + Assert.assertTrue("Incorrect Attachment Count", message.getAttachments().size() == 2); + Assert.assertEquals( + "Incorrect Attachment1 Name", + message.getAttachments().get(0).getName(), + attachmentName + ); + Assert.assertEquals( + "Incorrect Attachment2 Name", + message.getAttachments().get(1).getName(), + attachmentName2 + ); + } + + @Test + public void CarouselUnorderedAttachments() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String attachmentName1 = UUID.randomUUID().toString(); + Attachment attachment1 = new Attachment(); + attachment1.setName(attachmentName1); + + String attachmentName2 = UUID.randomUUID().toString(); + Attachment attachment2 = new Attachment(); + attachment2.setName(attachmentName2); + + Set multipleAttachments = new HashSet<>( + Arrays.asList(attachment1, attachment2) + ); + Activity message = MessageFactory.carousel( + new ArrayList<>(multipleAttachments), + text, + ssml, + inputHint + ); + + Set names = new HashSet<>(Arrays.asList(attachmentName1, attachmentName2)); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertTrue(message.getAttachmentLayout() == AttachmentLayoutTypes.CAROUSEL); + Assert.assertTrue("Incorrect Attachment Count", message.getAttachments().size() == 2); + Assert.assertTrue( + "Incorrect set of attachment names.", + names.containsAll(message.getAttachments().stream().map(Attachment::getName).collect(Collectors.toList())) + ); + } + + @Test + public void AttachmentMultiple() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String attachmentName = UUID.randomUUID().toString(); + Attachment a = new Attachment(); + a.setName(attachmentName); + + String attachmentName2 = UUID.randomUUID().toString(); + Attachment a2 = new Attachment(); + a2.setName(attachmentName2); + + List multipleAttachments = Arrays.asList(a, a2); + Activity message = MessageFactory.attachment(multipleAttachments, text, ssml, inputHint); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertTrue(message.getAttachmentLayout() == AttachmentLayoutTypes.LIST); + Assert.assertTrue("Incorrect Attachment Count", message.getAttachments().size() == 2); + Assert.assertEquals( + "Incorrect Attachment1 Name", + message.getAttachments().get(0).getName(), + attachmentName + ); + Assert.assertEquals( + "Incorrect Attachment2 Name", + message.getAttachments().get(1).getName(), + attachmentName2 + ); + } + + @Test + public void AttachmentMultipleUnordered() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + + String attachmentName1 = UUID.randomUUID().toString(); + Attachment attachment1 = new Attachment(); + attachment1.setName(attachmentName1); + + String attachmentName2 = UUID.randomUUID().toString(); + Attachment attachment2 = new Attachment(); + attachment2.setName(attachmentName2); + + Set multipleAttachments = new HashSet<>( + Arrays.asList(attachment1, attachment2) + ); + Activity message = MessageFactory.attachment( + new ArrayList<>(multipleAttachments), + text, + ssml, + inputHint + ); + + Set names = new HashSet<>(Arrays.asList(attachmentName1, attachmentName2)); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertSame(message.getAttachmentLayout(), AttachmentLayoutTypes.LIST); + Assert.assertEquals("Incorrect Attachment Count", 2, message.getAttachments().size()); + Assert.assertTrue( + "Incorrect set of attachment names.", + names.containsAll(message.getAttachments().stream().map(Attachment::getName).collect(Collectors.toList())) + ); + } + + @Test + public void ContentUrl() { + String text = UUID.randomUUID().toString(); + String ssml = UUID.randomUUID().toString(); + InputHints inputHint = InputHints.EXPECTING_INPUT; + String uri = "https://" + UUID.randomUUID().toString(); + String contentType = "image/jpeg"; + String name = UUID.randomUUID().toString(); + + Activity message = MessageFactory.contentUrl(uri, contentType, name, text, ssml, inputHint); + + Assert.assertEquals("Message Text does not match", text, message.getText()); + Assert.assertEquals("Incorrect Activity Type", ActivityTypes.MESSAGE, message.getType()); + Assert.assertEquals("InputHint does not match", inputHint, message.getInputHint()); + Assert.assertEquals("ssml text is incorrect", ssml, message.getSpeak()); + Assert.assertEquals(1, message.getAttachments().size()); + Assert.assertEquals( + "Incorrect Attachment1 Name", + message.getAttachments().get(0).getName(), + name + ); + Assert.assertSame( + "Incorrect contentType", + message.getAttachments().get(0).getContentType(), + contentType + ); + Assert.assertEquals("Incorrect Uri", message.getAttachments().get(0).getContentUrl(), uri); + } + + @Test + public void ValidateIMBackWithText() { + TestAdapter adapter = new TestAdapter(); + + BotCallbackHandler replyWithimBackBack = turnContext -> { + if (StringUtils.equals(turnContext.getActivity().getText(), "test")) { + CardAction card = new CardAction(); + card.setType(ActionTypes.IM_BACK); + card.setText("red"); + card.setTitle("redTitle"); + Activity activity = MessageFactory.suggestedCardActions( + Collections.singletonList(card), + "Select color" + ); + + turnContext.sendActivity(activity).join(); + } + return CompletableFuture.completedFuture(null); + }; + + Consumer validateIMBack = activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.MESSAGE)); + Assert.assertEquals("Select color", activity.getText()); + Assert.assertEquals( + "Incorrect Count", + 1, + activity.getSuggestedActions().getActions().size() + ); + Assert.assertSame( + "Incorrect Action Type", + activity.getSuggestedActions().getActions().get(0).getType(), + ActionTypes.IM_BACK + ); + Assert.assertEquals( + "incorrect text", + activity.getSuggestedActions().getActions().get(0).getText(), + "red" + ); + Assert.assertEquals( + "incorrect text", + activity.getSuggestedActions().getActions().get(0).getTitle(), + "redTitle" + ); + }; + + new TestFlow(adapter, replyWithimBackBack).send("test").assertReply( + validateIMBack, + "IMBack Did not validate" + ).startTest().join(); + } + + @Test + public void ValidateIMBackWithNoTest() { + TestAdapter adapter = new TestAdapter(); + + BotCallbackHandler replyWithimBackBack = turnContext -> { + if (StringUtils.equals(turnContext.getActivity().getText(), "test")) { + CardAction card = new CardAction(); + card.setType(ActionTypes.IM_BACK); + card.setText("red"); + card.setTitle("redTitle"); + Activity activity = MessageFactory.suggestedCardActions( + Collections.singletonList(card), + null + ); + + turnContext.sendActivity(activity).join(); + } + return CompletableFuture.completedFuture(null); + }; + + Consumer validateIMBack = activity -> { + Assert.assertTrue(activity.isType(ActivityTypes.MESSAGE)); + Assert.assertNull(activity.getText()); + Assert.assertEquals( + "Incorrect Count", + 1, + activity.getSuggestedActions().getActions().size() + ); + Assert.assertSame( + "Incorrect Action Type", + activity.getSuggestedActions().getActions().get(0).getType(), + ActionTypes.IM_BACK + ); + Assert.assertEquals( + "incorrect text", + activity.getSuggestedActions().getActions().get(0).getText(), + "red" + ); + Assert.assertEquals( + "incorrect text", + activity.getSuggestedActions().getActions().get(0).getTitle(), + "redTitle" + ); + }; + + new TestFlow(adapter, replyWithimBackBack).send("test").assertReply( + validateIMBack, + "IMBack Did not validate" + ).startTest().join(); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java new file mode 100644 index 000000000..6ded16347 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MiddlewareSetTest.java @@ -0,0 +1,491 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class MiddlewareSetTest { + private boolean wasCalled; + + @Test + public void NoMiddleware() { + try { + MiddlewareSet m = new MiddlewareSet(); + // No middleware. Should not explode. + m.receiveActivityWithStatus(null, null).join(); + Assert.assertTrue(true); + } catch (Throwable t) { + Assert.fail("No exception expected" + t.getMessage()); + } + } + + @Test + public void NestedSet_OnReceive() { + wasCalled = false; + + MiddlewareSet inner = new MiddlewareSet(); + inner.use(new AnonymousReceiveMiddleware((tc, nd) -> { + wasCalled = true; + return nd.next(); + })); + + MiddlewareSet outer = new MiddlewareSet(); + outer.use(inner); + + outer.receiveActivityWithStatus(null, null).join(); + + Assert.assertTrue("Inner Middleware Receive was not called.", wasCalled); + } + + @Test + public void NoMiddlewareWithDelegate() { + MiddlewareSet m = new MiddlewareSet(); + wasCalled = false; + + BotCallbackHandler cb = (ctx) -> { + wasCalled = true; + return CompletableFuture.completedFuture(null); + }; + + // No middleware. Should not explode. + m.receiveActivityWithStatus(null, cb).join(); + Assert.assertTrue("Delegate was not called", wasCalled); + } + + @Test + public void OneMiddlewareItem() { + WasCalledMiddleware simple = new WasCalledMiddleware(); + + wasCalled = false; + BotCallbackHandler cb = (ctx) -> { + wasCalled = true; + return CompletableFuture.completedFuture(null); + }; + + MiddlewareSet m = new MiddlewareSet(); + m.use(simple); + + Assert.assertFalse(simple.getCalled()); + m.receiveActivityWithStatus(null, cb).join(); + Assert.assertTrue(simple.getCalled()); + Assert.assertTrue("Delegate was not called", wasCalled); + } + + @Test + public void OneMiddlewareItemWithDelegate() { + WasCalledMiddleware simple = new WasCalledMiddleware(); + + MiddlewareSet m = new MiddlewareSet(); + m.use(simple); + + Assert.assertFalse(simple.getCalled()); + m.receiveActivityWithStatus(null, null).join(); + Assert.assertTrue(simple.getCalled()); + } + + @Test + public void BubbleUncaughtException() { + MiddlewareSet m = new MiddlewareSet(); + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + throw new CompletionException(new IllegalStateException("test")); + })); + + try { + m.receiveActivityWithStatus(null, null).join(); + Assert.assertFalse("Should never have gotten here", true); + } catch (CompletionException ce) { + Assert.assertTrue(ce.getCause() instanceof IllegalStateException); + } + } + + @Test + public void TwoMiddlewareItems() { + WasCalledMiddleware one = new WasCalledMiddleware(); + WasCalledMiddleware two = new WasCalledMiddleware(); + + MiddlewareSet m = new MiddlewareSet(); + m.use(one); + m.use(two); + + m.receiveActivityWithStatus(null, null).join(); + Assert.assertTrue(one.getCalled()); + Assert.assertTrue(two.getCalled()); + } + + @Test + public void TwoMiddlewareItemsWithDelegate() { + WasCalledMiddleware one = new WasCalledMiddleware(); + WasCalledMiddleware two = new WasCalledMiddleware(); + + final int[] called = { 0 }; + BotCallbackHandler cb = (context) -> { + called[0]++; + return CompletableFuture.completedFuture(null); + }; + + MiddlewareSet m = new MiddlewareSet(); + m.use(one); + m.use(two); + + m.receiveActivityWithStatus(null, cb).join(); + Assert.assertTrue(one.getCalled()); + Assert.assertTrue(two.getCalled()); + Assert.assertTrue("Incorrect number of calls to Delegate", called[0] == 1); + } + + @Test + public void TwoMiddlewareItemsInOrder() { + final boolean[] called1 = { false }; + final boolean[] called2 = { false }; + + CallMeMiddleware one = new CallMeMiddleware(() -> { + Assert.assertFalse("Second Middleware was called", called2[0]); + called1[0] = true; + }); + + CallMeMiddleware two = new CallMeMiddleware(() -> { + Assert.assertTrue("First Middleware was not called", called1[0]); + called2[0] = true; + }); + + MiddlewareSet m = new MiddlewareSet(); + m.use(one); + m.use(two); + + m.receiveActivityWithStatus(null, null).join(); + + Assert.assertTrue(called1[0]); + Assert.assertTrue(called2[0]); + } + + @Test + public void Status_OneMiddlewareRan() { + final boolean[] called1 = { false }; + + CallMeMiddleware one = new CallMeMiddleware(() -> called1[0] = true); + + MiddlewareSet m = new MiddlewareSet(); + m.use(one); + + // The middleware in this pipeline calls next(), so the delegate should be + // called + final boolean[] didAllRun = { false }; + BotCallbackHandler cb = (context) -> { + didAllRun[0] = true; + return CompletableFuture.completedFuture(null); + }; + m.receiveActivityWithStatus(null, cb).join(); + + Assert.assertTrue(called1[0]); + Assert.assertTrue(didAllRun[0]); + } + + @Test + public void Status_RunAtEndEmptyPipeline() { + MiddlewareSet m = new MiddlewareSet(); + final boolean[] didAllRun = { false }; + BotCallbackHandler cb = (context) -> { + didAllRun[0] = true; + return CompletableFuture.completedFuture(null); + }; + + // This middleware pipeline has no entries. This should result in + // the status being TRUE. + m.receiveActivityWithStatus(null, cb); + + Assert.assertTrue(didAllRun[0]); + + } + + @Test + public void Status_TwoItemsOneDoesNotCallNext() { + final boolean[] called1 = { false }; + final boolean[] called2 = { false }; + + CallMeMiddleware one = new CallMeMiddleware(() -> { + Assert.assertFalse("Second Middleware was called", called2[0]); + called1[0] = true; + }); + + DoNotCallNextMiddleware two = new DoNotCallNextMiddleware(() -> { + Assert.assertTrue("First Middleware was not called", called1[0]); + called2[0] = true; + }); + + MiddlewareSet m = new MiddlewareSet(); + m.use(one); + m.use(two); + + boolean[] didAllRun = { false }; + BotCallbackHandler cb = (context) -> { + didAllRun[0] = true; + return CompletableFuture.completedFuture(null); + }; + + m.receiveActivityWithStatus(null, cb).join(); + + Assert.assertTrue(called1[0]); + Assert.assertTrue(called2[0]); + + // The 2nd middleware did not call next, so the "final" action should not have + // run. + Assert.assertFalse(didAllRun[0]); + } + + @Test + public void Status_OneEntryThatDoesNotCallNext() { + final boolean[] called1 = { false }; + + DoNotCallNextMiddleware one = new DoNotCallNextMiddleware(() -> called1[0] = true); + + MiddlewareSet m = new MiddlewareSet(); + m.use(one); + + // The middleware in this pipeline DOES NOT call next(), so this must not be + // called + boolean[] didAllRun = { false }; + BotCallbackHandler cb = (context) -> { + didAllRun[0] = true; + return CompletableFuture.completedFuture(null); + }; + m.receiveActivityWithStatus(null, cb); + + Assert.assertTrue(called1[0]); + + // Our "Final" action MUST NOT have been called, as the Middlware Pipeline + // didn't complete. + Assert.assertFalse(didAllRun[0]); + } + + @Test + public void AnonymousMiddleware() { + final boolean[] didRun = { false }; + + MiddlewareSet m = new MiddlewareSet(); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + didRun[0] = true; + return nd.next(); + })); + + Assert.assertFalse(didRun[0]); + m.receiveActivityWithStatus(null, null).join(); + Assert.assertTrue(didRun[0]); + } + + @Test + public void TwoAnonymousMiddleware() { + final boolean[] didRun1 = { false }; + final boolean[] didRun2 = { false }; + + MiddlewareSet m = new MiddlewareSet(); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + didRun1[0] = true; + return nd.next(); + })); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + didRun2[0] = true; + return nd.next(); + })); + + m.receiveActivityWithStatus(null, null).join(); + + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void TwoAnonymousMiddlewareInOrder() { + final boolean[] didRun1 = { false }; + final boolean[] didRun2 = { false }; + + MiddlewareSet m = new MiddlewareSet(); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + Assert.assertFalse("Looks like the 2nd one has already run", didRun2[0]); + didRun1[0] = true; + return nd.next(); + })); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + Assert.assertTrue("Looks like the 1nd one has not yet run", didRun1[0]); + didRun2[0] = true; + return nd.next(); + })); + + m.receiveActivityWithStatus(null, null).join(); + + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void MixedMiddlewareInOrderAnonymousFirst() { + final boolean[] didRun1 = { false }; + final boolean[] didRun2 = { false }; + + MiddlewareSet m = new MiddlewareSet(); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + Assert.assertFalse("First middleware already ran", didRun1[0]); + Assert.assertFalse("Looks like the second middleware was already run", didRun2[0]); + didRun1[0] = true; + CompletableFuture result = nd.next(); + Assert.assertTrue("Second middleware should have completed running", didRun2[0]); + return result; + })); + + ActionDel act = () -> { + Assert.assertTrue("First middleware should have already been called", didRun1[0]); + Assert.assertFalse("Second middleware should not have been invoked yet", didRun2[0]); + didRun2[0] = true; + }; + m.use(new CallMeMiddleware(act)); + + m.receiveActivityWithStatus(null, null).join(); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void MixedMiddlewareInOrderAnonymousLast() { + final boolean[] didRun1 = { false }; + final boolean[] didRun2 = { false }; + + MiddlewareSet m = new MiddlewareSet(); + + ActionDel act = () -> { + Assert.assertFalse("First middleware should not have already been called", didRun1[0]); + Assert.assertFalse("Second middleware should not have been invoked yet", didRun2[0]); + didRun1[0] = true; + }; + m.use(new CallMeMiddleware(act)); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + Assert.assertTrue("First middleware has not been run yet", didRun1[0]); + didRun2[0] = true; + return nd.next(); + })); + + m.receiveActivityWithStatus(null, null); + + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + } + + @Test + public void RunCodeBeforeAndAfter() { + final boolean[] didRun1 = { false }; + final boolean[] codeafter2run = { false }; + final boolean[] didRun2 = { false }; + + MiddlewareSet m = new MiddlewareSet(); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + Assert.assertFalse("Looks like the 1st middleware has already run", didRun1[0]); + didRun1[0] = true; + CompletableFuture result = nd.next(); + Assert.assertTrue("The 2nd middleware should have run now.", didRun1[0]); + codeafter2run[0] = true; + return result; + })); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> { + Assert.assertTrue("Looks like the 1st middleware has not been run", didRun1[0]); + Assert.assertFalse( + "The code that runs after middleware 2 is complete has already run.", + codeafter2run[0] + ); + didRun2[0] = true; + return nd.next(); + })); + + m.receiveActivityWithStatus(null, null).join(); + Assert.assertTrue(didRun1[0]); + Assert.assertTrue(didRun2[0]); + Assert.assertTrue(codeafter2run[0]); + } + + @Test + public void CatchAnExceptionViaMiddleware() { + MiddlewareSet m = new MiddlewareSet(); + final boolean[] caughtException = { false }; + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> CompletableFuture.supplyAsync(() -> { + System.out.println("First Middleware"); + return null; + }).thenCompose((result) -> nd.next()).exceptionally(ex -> { + Assert.assertTrue(ex instanceof CompletionException); + Assert.assertTrue(ex.getCause() instanceof InterruptedException); + System.out.println("First Middleware caught"); + caughtException[0] = true; + return null; + }))); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> CompletableFuture.supplyAsync(() -> { + System.out.println("Second Middleware"); + return null; + }).thenCompose(result -> nd.next()))); + + m.use(new AnonymousReceiveMiddleware((tc, nd) -> CompletableFuture.supplyAsync(() -> { + System.out.println("Third Middleware will throw"); + throw new CompletionException(new InterruptedException("test")); + }).thenCompose(result -> nd.next()))); + + m.receiveActivityWithStatus(null, null).join(); + + Assert.assertTrue(caughtException[0]); + } + + private static class WasCalledMiddleware implements Middleware { + boolean called = false; + + public boolean getCalled() { + return this.called; + } + + public void setCalled(boolean called) { + this.called = called; + } + + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + setCalled(true); + return next.next(); + } + } + + private static class DoNotCallNextMiddleware implements Middleware { + private final ActionDel _callMe; + + public DoNotCallNextMiddleware(ActionDel callMe) { + _callMe = callMe; + } + + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + _callMe.CallMe(); + // DO NOT call NEXT + return CompletableFuture.completedFuture(null); + } + } + + private static class CallMeMiddleware implements Middleware { + private ActionDel callMe; + + public CallMeMiddleware(ActionDel callme) { + this.callMe = callme; + } + + @Override + public CompletableFuture onTurn(TurnContext context, NextDelegate next) { + this.callMe.CallMe(); + return next.next(); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MockAppCredentials.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MockAppCredentials.java new file mode 100644 index 000000000..19c23e5c7 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MockAppCredentials.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.Authenticator; + +public class MockAppCredentials extends AppCredentials { + + private String token; + + public MockAppCredentials(String token) { + super(null); + this.token = token; + } + + @Override + public CompletableFuture getToken() { + CompletableFuture result; + + result = new CompletableFuture(); + result.complete(this.token); + return result; + } + + /** + * Returns an appropriate Authenticator that is provided by a subclass. + * + * @return An Authenticator object. + * @throws MalformedURLException If the endpoint isn't valid. + */ + protected Authenticator buildAuthenticator(){ + return null; + }; + +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MockConnectorClient.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MockConnectorClient.java new file mode 100644 index 000000000..aa06621de --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MockConnectorClient.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Attachments; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; + +public class MockConnectorClient implements ConnectorClient { + + private AppCredentials credentials; + private String userAgent; + + public MockConnectorClient(String userAgent, AppCredentials credentials) { + this.userAgent = userAgent; + this.credentials = credentials; + } + + private MemoryConversations conversations = new MemoryConversations(); + + @Override + public RestClient getRestClient() { + return null; + } + + @Override + public String baseUrl() { + return null; + } + + @Override + public ServiceClientCredentials credentials() { + return credentials; + } + + @Override + public String getUserAgent() { + return userAgent; + } + + @Override + public String getAcceptLanguage() { + return null; + } + + @Override + public void setAcceptLanguage(String acceptLanguage) { + + } + + @Override + public int getLongRunningOperationRetryTimeout() { + return 0; + } + + @Override + public void setLongRunningOperationRetryTimeout(int timeout) { + + } + + @Override + public boolean getGenerateClientRequestId() { + return false; + } + + @Override + public void setGenerateClientRequestId(boolean generateClientRequestId) { + + } + + @Override + public Attachments getAttachments() { + return null; + } + + @Override + public Conversations getConversations() { + return conversations; + } + + @Override + public void close() throws Exception { + + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/OnTurnErrorTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/OnTurnErrorTests.java new file mode 100644 index 000000000..d7324212b --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/OnTurnErrorTests.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +public class OnTurnErrorTests { + @Test + public void OnTurnError_Test() { + TestAdapter adapter = new TestAdapter(); + adapter.setOnTurnError(((turnContext, exception) -> { + if (exception instanceof NotImplementedException) { + return turnContext.sendActivity( + turnContext.getActivity().createReply(exception.getMessage()) + ).thenApply(resourceResponse -> null); + } else { + return turnContext.sendActivity("Unexpected exception").thenApply( + resourceResponse -> null + ); + } + })); + + new TestFlow(adapter, (turnContext -> { + if (StringUtils.equals(turnContext.getActivity().getText(), "foo")) { + turnContext.sendActivity(turnContext.getActivity().getText()).join(); + } + + if ( + StringUtils.equals(turnContext.getActivity().getText(), "NotImplementedException") + ) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new NotImplementedException("Test")); + return result; + } + + return CompletableFuture.completedFuture(null); + }) + ).send("foo").assertReply("foo", "passthrough").send("NotImplementedException").assertReply( + "Test" + ).startTest().join(); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SetSpeakMiddlewareTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SetSpeakMiddlewareTests.java new file mode 100644 index 000000000..a079aacc5 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SetSpeakMiddlewareTests.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; + +import org.junit.Assert; +import org.junit.Test; + +public class SetSpeakMiddlewareTests { + + @Test + public void NoFallback() { + TestAdapter adapter = new TestAdapter(createConversation("NoFallback")) + .use(new SetSpeakMiddleware("male", false)); + + new TestFlow(adapter, turnContext -> { + Activity activity = MessageFactory.text("OK"); + + return turnContext.sendActivity(activity).thenApply(result -> null); + }).send("foo").assertReply(obj -> { + Activity activity = (Activity) obj; + Assert.assertNull(activity.getSpeak()); + }).startTest().join(); + } + + // fallback instanceof true, for any ChannelId other than emulator, + // directlinespeech, or telephony should + // just set Activity.Speak to Activity.Text if Speak instanceof empty. + @Test + public void FallbackNullSpeak() { + TestAdapter adapter = new TestAdapter(createConversation("NoFallback")) + .use(new SetSpeakMiddleware("male", true)); + + new TestFlow(adapter, turnContext -> { + Activity activity = MessageFactory.text("OK"); + + return turnContext.sendActivity(activity).thenApply(result -> null); + }).send("foo").assertReply(obj -> { + Activity activity = (Activity) obj; + Assert.assertEquals(activity.getText(), activity.getSpeak()); + }).startTest().join(); + } + + // fallback instanceof true, for any ChannelId other than emulator, + // directlinespeech, or telephony should + // leave a non-empty Speak unchanged. + @Test + public void FallbackWithSpeak() { + TestAdapter adapter = new TestAdapter(createConversation("Fallback")) + .use(new SetSpeakMiddleware("male", true)); + + new TestFlow(adapter, turnContext -> { + Activity activity = MessageFactory.text("OK"); + activity.setSpeak("speak value"); + + return turnContext.sendActivity(activity).thenApply(result -> null); + }).send("foo").assertReply(obj -> { + Activity activity = (Activity) obj; + Assert.assertEquals("speak value", activity.getSpeak()); + }).startTest().join(); + } + + @Test + public void AddVoiceEmulator() { + AddVoice(Channels.EMULATOR); + } + + @Test + public void AddVoiceDirectlineSpeech() { + AddVoice(Channels.DIRECTLINESPEECH); + } + + @Test + public void AddVoiceTelephony() { + AddVoice("telephony"); + } + + + // Voice instanceof added to Speak property. + public void AddVoice(String channelId) { + TestAdapter adapter = new TestAdapter(createConversation("Fallback", channelId)) + .use(new SetSpeakMiddleware("male", true)); + + new TestFlow(adapter, turnContext -> { + Activity activity = MessageFactory.text("OK"); + + return turnContext.sendActivity(activity).thenApply(result -> null); + }).send("foo").assertReply(obj -> { + Activity activity = (Activity) obj; + Assert.assertEquals("OK", + activity.getSpeak()); + }).startTest().join(); + } + + @Test + public void AddNoVoiceEmulator() { + AddNoVoice(Channels.EMULATOR); + } + + @Test + public void AddNoVoiceDirectlineSpeech() { + AddNoVoice(Channels.DIRECTLINESPEECH); + } + + @Test + public void AddNoVoiceTelephony() { + AddNoVoice(Channels.TELEPHONY); + } + + + // With no 'voice' specified, the Speak property instanceof unchanged. + public void AddNoVoice(String channelId) { + TestAdapter adapter = new TestAdapter(createConversation("Fallback", channelId)) + .use(new SetSpeakMiddleware(null, true)); + + new TestFlow(adapter, turnContext -> { + Activity activity = MessageFactory.text("OK"); + + return turnContext.sendActivity(activity).thenApply(result -> null); + }).send("foo").assertReply(obj -> { + Activity activity = (Activity) obj; + Assert.assertEquals("OK", + activity.getSpeak()); + }).startTest().join(); + } + + + private static ConversationReference createConversation(String name) { + return createConversation(name, "User1", "Bot", "test"); + } + + private static ConversationReference createConversation(String name, String channelId) { + return createConversation(name, "User1", "Bot", channelId); + } + + private static ConversationReference createConversation(String name, String user, String bot, String channelId) { + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setChannelId(channelId); + conversationReference.setServiceUrl("https://test.com"); + conversationReference.setConversation(new ConversationAccount(false, name, name)); + conversationReference.setUser(new ChannelAccount(user.toLowerCase(), user)); + conversationReference.setBot(new ChannelAccount(bot.toLowerCase(), bot)); + conversationReference.setLocale("en-us"); + return conversationReference; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ShowTypingMiddlewareTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ShowTypingMiddlewareTests.java new file mode 100644 index 000000000..daa681438 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/ShowTypingMiddlewareTests.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +public class ShowTypingMiddlewareTests { + @Test + public void ShowTyping_TestMiddleware_1_Second_Interval() { + TestAdapter adapter = new TestAdapter().use(new ShowTypingMiddleware(100, 1000)); + + new TestFlow(adapter, (turnContext -> { + try { + Thread.sleep(2500); + } catch (InterruptedException e) { + // do nothing + } + + Assert.assertFalse(turnContext.getResponded()); + + turnContext.sendActivity("Message send after delay").join(); + return CompletableFuture.completedFuture(null); + }) + ).send("foo").assertReply(this::validateTypingActivity).assertReply( + this::validateTypingActivity + ).assertReply(this::validateTypingActivity).assertReply( + "Message send after delay" + ).startTest().join(); + } + + @Test + public void ShowTyping_TestMiddleware_Context_Completes_Before_Typing_Interval() { + TestAdapter adapter = new TestAdapter().use(new ShowTypingMiddleware(100, 5000)); + + new TestFlow(adapter, (turnContext -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + // do nothing + } + + turnContext.sendActivity("Message send after delay").join(); + return CompletableFuture.completedFuture(null); + }) + ).send("foo").assertReply(this::validateTypingActivity).assertReply( + "Message send after delay" + ).startTest().join(); + } + + @Test + public void ShowTyping_TestMiddleware_ImmediateResponse_5SecondInterval() { + TestAdapter adapter = new TestAdapter().use(new ShowTypingMiddleware(2000, 5000)); + + new TestFlow(adapter, (turnContext -> { + turnContext.sendActivity("Message send after delay").join(); + return CompletableFuture.completedFuture(null); + })).send("foo").assertReply("Message send after delay").startTest().join(); + } + + @Test(expected = IllegalArgumentException.class) + public void ShowTyping_TestMiddleware_NegativeDelay() { + TestAdapter adapter = new TestAdapter().use(new ShowTypingMiddleware(-100, 5000)); + } + + @Test(expected = IllegalArgumentException.class) + public void ShowTyping_TestMiddleware_ZeroFrequency() { + TestAdapter adapter = new TestAdapter().use(new ShowTypingMiddleware(-100, 0)); + } + + @Test(expected = IllegalArgumentException.class) + public void ShowTyping_TestMiddleware_NegativePerion() { + TestAdapter adapter = new TestAdapter().use(new ShowTypingMiddleware(500, -500)); + } + + private void validateTypingActivity(Activity obj) { + if (!obj.isType(ActivityTypes.TYPING)) { + throw new RuntimeException("Activity was not of type TypingActivity"); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java new file mode 100644 index 000000000..62d4a1f4b --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SimpleAdapter.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import org.checkerframework.checker.units.qual.C; +import org.junit.Assert; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class SimpleAdapter extends BotAdapter { + private Consumer> callOnSend = null; + private Consumer callOnUpdate = null; + private Consumer callOnDelete = null; + + // Callback Function but doesn't need to be. Avoid java legacy type erasure + public SimpleAdapter(Consumer> callOnSend) { + this(callOnSend, null, null); + } + + public SimpleAdapter( + Consumer> callOnSend, + Consumer callOnUpdate + ) { + this(callOnSend, callOnUpdate, null); + } + + public SimpleAdapter( + Consumer> callOnSend, + Consumer callOnUpdate, + Consumer callOnDelete + ) { + this.callOnSend = callOnSend; + this.callOnUpdate = callOnUpdate; + this.callOnDelete = callOnDelete; + } + + public SimpleAdapter() { + + } + + @Override + public CompletableFuture sendActivities( + TurnContext context, + List activities + ) { + Assert.assertNotNull("SimpleAdapter.deleteActivity: missing reference", activities); + Assert.assertTrue( + "SimpleAdapter.sendActivities: empty activities array.", + activities.size() > 0 + ); + + if (this.callOnSend != null) + this.callOnSend.accept(activities); + + List responses = new ArrayList(); + for (Activity activity : activities) { + responses.add(new ResourceResponse(activity.getId())); + } + ResourceResponse[] result = new ResourceResponse[responses.size()]; + return CompletableFuture.completedFuture(responses.toArray(result)); + } + + @Override + public CompletableFuture updateActivity( + TurnContext context, + Activity activity + ) { + Assert.assertNotNull("SimpleAdapter.updateActivity: missing activity", activity); + if (this.callOnUpdate != null) + this.callOnUpdate.accept(activity); + return CompletableFuture.completedFuture(new ResourceResponse(activity.getId())); + } + + @Override + public CompletableFuture deleteActivity( + TurnContext context, + ConversationReference reference + ) { + Assert.assertNotNull("SimpleAdapter.deleteActivity: missing reference", reference); + if (callOnDelete != null) + this.callOnDelete.accept(reference); + return CompletableFuture.completedFuture(null); + } + + public CompletableFuture processRequest(Activity activity, BotCallbackHandler callback) { + CompletableFuture pipelineResult = new CompletableFuture<>(); + + try (TurnContextImpl context = new TurnContextImpl(this, activity)) { + pipelineResult = runPipeline(context, callback); + } catch (Exception e) { + pipelineResult.completeExceptionally(e); + } + + return pipelineResult; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SkillConversationIdFactoryTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SkillConversationIdFactoryTests.java new file mode 100644 index 000000000..db67c97b0 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/SkillConversationIdFactoryTests.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.builder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.UUID; + +import com.microsoft.bot.builder.skills.BotFrameworkSkill; +import com.microsoft.bot.builder.skills.SkillConversationIdFactory; +import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions; +import com.microsoft.bot.builder.skills.SkillConversationReference; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; + +import org.junit.Test; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; + + +public class SkillConversationIdFactoryTests { + + private static final String SERVICE_URL = "http://testbot.com/api/messages"; + private final String skillId = "skill"; + + private final SkillConversationIdFactory skillConversationIdFactory = + new SkillConversationIdFactory(new MemoryStorage()); + private final String applicationId = UUID.randomUUID().toString(); + private final String botId = UUID.randomUUID().toString(); + + @Test + public void SkillConversationIdFactoryHappyPath() { + ConversationReference conversationReference = buildConversationReference(); + + // Create skill conversation + SkillConversationIdFactoryOptions options = new SkillConversationIdFactoryOptions(); + options.setActivity(buildMessageActivity(conversationReference)); + options.setBotFrameworkSkill(this.buildBotFrameworkSkill()); + options.setFromBotId(botId); + options.setFromBotOAuthScope(botId); + + + String skillConversationId = skillConversationIdFactory.createSkillConversationId(options).join(); + + Assert.assertFalse(StringUtils.isBlank(skillConversationId)); + + // Retrieve skill conversation + SkillConversationReference retrievedConversationReference = + skillConversationIdFactory.getSkillConversationReference(skillConversationId).join(); + + // Delete + skillConversationIdFactory.deleteConversationReference(skillConversationId).join(); + + // Retrieve again + SkillConversationReference deletedConversationReference = + skillConversationIdFactory.getSkillConversationReference(skillConversationId).join(); + + Assert.assertNotNull(retrievedConversationReference); + Assert.assertNotNull(retrievedConversationReference.getConversationReference()); + Assert.assertTrue(compareConversationReferences(conversationReference, + retrievedConversationReference.getConversationReference())); + Assert.assertNull(deletedConversationReference); + } + + @Test + public void IdIsUniqueEachTime() { + ConversationReference conversationReference = buildConversationReference(); + + // Create skill conversation + SkillConversationIdFactoryOptions options1 = new SkillConversationIdFactoryOptions(); + options1.setActivity(buildMessageActivity(conversationReference)); + options1.setBotFrameworkSkill(buildBotFrameworkSkill()); + options1.setFromBotId(botId); + options1.setFromBotOAuthScope(botId); + + String firstId = skillConversationIdFactory.createSkillConversationId(options1).join(); + + + SkillConversationIdFactoryOptions options2 = new SkillConversationIdFactoryOptions(); + options2.setActivity(buildMessageActivity(conversationReference)); + options2.setBotFrameworkSkill(buildBotFrameworkSkill()); + options2.setFromBotId(botId); + options2.setFromBotOAuthScope(botId); + + String secondId = skillConversationIdFactory.createSkillConversationId(options2).join(); + + // Ensure that we get a different conversationId each time we call CreateSkillConversationIdAsync + Assert.assertNotEquals(firstId, secondId); + } + + + + private static ConversationReference buildConversationReference() { + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setConversation(new ConversationAccount(UUID.randomUUID().toString())); + conversationReference.setServiceUrl(SERVICE_URL); + return conversationReference; + } + + private static Activity buildMessageActivity(ConversationReference conversationReference) { + if (conversationReference == null) { + throw new IllegalArgumentException("conversationReference cannot be null."); + } + + Activity activity = Activity.createMessageActivity(); + activity.applyConversationReference(conversationReference); + + return activity; + } + + private BotFrameworkSkill buildBotFrameworkSkill() { + BotFrameworkSkill skill = new BotFrameworkSkill(); + skill.setAppId(applicationId); + skill.setId(skillId); + try { + skill.setSkillEndpoint(new URI(SERVICE_URL)); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return skill; + } + + private static boolean compareConversationReferences( + ConversationReference reference1, + ConversationReference reference2 + ) { + return reference1.getConversation().getId() == reference2.getConversation().getId() + && reference1.getServiceUrl() == reference2.getServiceUrl(); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/StorageBaseTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/StorageBaseTests.java new file mode 100644 index 000000000..51048dad2 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/StorageBaseTests.java @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import org.junit.Assert; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class StorageBaseTests { + protected void readUnknownTest(Storage storage) { + Map result = storage.read(new String[] { "unknown" }).join(); + Assert.assertNotNull("result should not be null", result); + Assert.assertNull("\"unknown\" key should have returned no value", result.get("unknown")); + } + + protected void createObjectTest(Storage storage) { + Map storeItems = new HashMap(); + storeItems.put("createPoco", new PocoItem("1")); + storeItems.put("createPocoStoreItem", new PocoStoreItem("1")); + + storage.write(storeItems).join(); + + Map readStoreItems = storage.read( + storeItems.keySet().toArray(new String[storeItems.size()]) + ).join(); + + Assert.assertTrue(readStoreItems.get("createPoco") instanceof PocoItem); + Assert.assertTrue(readStoreItems.get("createPocoStoreItem") instanceof PocoStoreItem); + + PocoItem createPoco = (PocoItem) readStoreItems.get("createPoco"); + + Assert.assertNotNull("createPoco should not be null", createPoco); + Assert.assertEquals("createPoco.id should be 1", "1", createPoco.getId()); + + PocoStoreItem createPocoStoreItem = (PocoStoreItem) readStoreItems.get( + "createPocoStoreItem" + ); + + Assert.assertNotNull("createPocoStoreItem should not be null", createPocoStoreItem); + Assert.assertEquals("createPocoStoreItem.id should be 1", "1", createPocoStoreItem.getId()); + Assert.assertNotNull( + "createPocoStoreItem.eTag should not be null", + createPocoStoreItem.getETag() + ); + } + + protected void handleCrazyKeys(Storage storage) { + String key = "!@#$%^&*()~/\\><,.?';\"`~"; + PocoStoreItem storeItem = new PocoStoreItem("1"); + Map dict = new HashMap(); + dict.put(key, storeItem); + storage.write(dict).join(); + Map storeItems = storage.read(new String[] { key }).join(); + + PocoStoreItem pocoStoreItem = (PocoStoreItem) storeItems.get(key); + + Assert.assertNotNull(pocoStoreItem); + Assert.assertEquals("1", pocoStoreItem.getId()); + } + + protected void updateObjectTest(Storage storage) { + Map dict = new HashMap(); + dict.put("pocoItem", new PocoItem("1", 1)); + dict.put("pocoStoreItem", new PocoStoreItem("1", 1)); + + storage.write(dict).join(); + Map loadedStoreItems = storage.read( + new String[] { "pocoItem", "pocoStoreItem" } + ).join(); + + PocoItem updatePocoItem = (PocoItem) loadedStoreItems.get("pocoItem"); + PocoStoreItem updatePocoStoreItem = (PocoStoreItem) loadedStoreItems.get("pocoStoreItem"); + Assert.assertNotNull( + "updatePocoStoreItem.eTag should not be null", + updatePocoStoreItem.getETag() + ); + + // 2nd write should work, because we have new etag, or no etag + updatePocoItem.setCount(updatePocoItem.getCount() + 1); + updatePocoStoreItem.setCount(updatePocoStoreItem.getCount() + 1); + + storage.write(loadedStoreItems).join(); + + Map reloadedStoreItems = storage.read( + new String[] { "pocoItem", "pocoStoreItem" } + ).join(); + + PocoItem reloeadedUpdatePocoItem = (PocoItem) reloadedStoreItems.get("pocoItem"); + PocoStoreItem reloadedUpdatePocoStoreItem = (PocoStoreItem) reloadedStoreItems.get( + "pocoStoreItem" + ); + + Assert.assertNotNull( + "reloadedUpdatePocoStoreItem.eTag should not be null", + reloadedUpdatePocoStoreItem.getETag() + ); + Assert.assertNotEquals( + "updatePocoItem.eTag should be different", + updatePocoStoreItem.getETag(), + reloadedUpdatePocoStoreItem.getETag() + ); + Assert.assertEquals( + "reloeadedUpdatePocoItem.Count should be 2", + 2, + reloeadedUpdatePocoItem.getCount() + ); + Assert.assertEquals( + "reloadedUpdatePocoStoreItem.Count should be 2", + 2, + reloadedUpdatePocoStoreItem.getCount() + ); + + try { + updatePocoItem.setCount(123); + HashMap pocoList = new HashMap(); + pocoList.put("pocoItem", updatePocoItem); + storage.write(pocoList).join(); + } catch (Throwable t) { + Assert.fail("Should not throw exception on write with pocoItem"); + } + + try { + updatePocoStoreItem.setCount(123); + HashMap pocoList = new HashMap(); + pocoList.put("pocoStoreItem", updatePocoStoreItem); + storage.write(pocoList).join(); + + Assert.fail( + "Should have thrown exception on write with store item because of old etag" + ); + } catch (Throwable t) { + + } + + Map reloadedStoreItems2 = storage.read( + new String[] { "pocoItem", "pocoStoreItem" } + ).join(); + + PocoItem reloadedPocoItem2 = (PocoItem) reloadedStoreItems2.get("pocoItem"); + PocoStoreItem reloadedPocoStoreItem2 = (PocoStoreItem) reloadedStoreItems2.get( + "pocoStoreItem" + ); + + Assert.assertEquals(123, reloadedPocoItem2.getCount()); + Assert.assertEquals(2, reloadedPocoStoreItem2.getCount()); + + // write with wildcard etag should work + reloadedPocoItem2.setCount(100); + reloadedPocoStoreItem2.setCount(100); + reloadedPocoStoreItem2.setETag("*"); + HashMap pocoList = new HashMap(); + pocoList.put("pocoItem", reloadedPocoItem2); + pocoList.put("pocoStoreItem", reloadedPocoStoreItem2); + storage.write(pocoList).join(); + + Map reloadedStoreItems3 = storage.read( + new String[] { "pocoItem", "pocoStoreItem" } + ).join(); + + Assert.assertEquals(100, ((PocoItem) reloadedStoreItems3.get("pocoItem")).getCount()); + Assert.assertEquals( + 100, + ((PocoStoreItem) reloadedStoreItems3.get("pocoStoreItem")).getCount() + ); + + // write with empty etag should not work + try { + PocoStoreItem reloadedStoreItem4 = (PocoStoreItem) storage.read( + new String[] { "pocoItem", "pocoStoreItem" } + ).join().get("pocoStoreItem"); + + reloadedStoreItem4.setETag(""); + + HashMap pocoList2 = new HashMap(); + pocoList2.put("pocoStoreItem", reloadedStoreItem4); + storage.write(pocoList2).join(); + + Assert.fail( + "Should have thrown exception on write with storeitem because of empty etag" + ); + } catch (Throwable t) { + + } + + Map finalStoreItems = storage.read( + new String[] { "pocoItem", "pocoStoreItem" } + ).join(); + Assert.assertEquals(100, ((PocoItem) finalStoreItems.get("pocoItem")).getCount()); + Assert.assertEquals(100, ((PocoStoreItem) finalStoreItems.get("pocoStoreItem")).getCount()); + } + + protected void deleteObjectTest(Storage storage) { + Map dict = new HashMap(); + dict.put("delete1", new PocoStoreItem("1", 1)); + + storage.write(dict).join(); + + Map storeItems = storage.read(new String[] { "delete1" }).join(); + PocoStoreItem storeItem = (PocoStoreItem) storeItems.get("delete1"); + + Assert.assertNotNull("etag should be set", storeItem.getETag()); + Assert.assertEquals(1, storeItem.getCount()); + + storage.delete(new String[] { "delete1" }).join(); + + Map reloadedStoreItems = storage.read(new String[] { "delete1" }).join(); + Assert.assertEquals( + "no store item should have been found because it was deleted", + 0, + reloadedStoreItems.size() + ); + } + + protected void deleteUnknownObjectTest(Storage storage) { + storage.delete(new String[] { "unknown_key" }).join(); + } + + protected void statePersistsThroughMultiTurn(Storage storage) { + UserState userState = new UserState(storage); + StatePropertyAccessor testProperty = userState.createProperty("test"); + TestAdapter adapter = new TestAdapter() + .use(new AutoSaveStateMiddleware(userState)); + + new TestFlow(adapter, context -> { + TestPocoState state = testProperty.get(context, TestPocoState::new).join(); + Assert.assertNotNull(state); + switch (context.getActivity().getText()) { + case "set value": + state.setValue("test"); + context.sendActivity("value saved").join(); + break; + case "get value": + context.sendActivity(state.getValue()).join(); + break; + } + + return CompletableFuture.completedFuture(null); + }) + .test("set value", "value saved") + .test("get value", "test") + .startTest().join(); + } + + private static class PocoItem { + public PocoItem() { + + } + + public PocoItem(String withId) { + id = withId; + } + + public PocoItem( + String withId, + int withCount + ) { + id = withId; + count = withCount; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public String[] getExtraBytes() { + return extraBytes; + } + + public void setExtraBytes(String[] extraBytes) { + this.extraBytes = extraBytes; + } + + private String id; + private int count; + private String[] extraBytes; + } + + private static class PocoStoreItem implements StoreItem { + private String id; + private int count; + private String eTag; + + public PocoStoreItem() { + + } + + public PocoStoreItem(String withId) { + id = withId; + } + + public PocoStoreItem( + String withId, + int withCount + ) { + id = withId; + count = withCount; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + @Override + public String getETag() { + return eTag; + } + + @Override + public void setETag(String withETag) { + eTag = withETag; + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TelemetryMiddlewareTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TelemetryMiddlewareTests.java new file mode 100644 index 000000000..605c34581 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TelemetryMiddlewareTests.java @@ -0,0 +1,767 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.teams.TeamInfo; +import com.microsoft.bot.schema.teams.TeamsChannelData; +import com.microsoft.bot.schema.teams.TenantInfo; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TelemetryMiddlewareTests { + @Captor + ArgumentCaptor eventNameCaptor; + + @Captor + ArgumentCaptor> propertiesCaptor; + + @Test + public void Telemetry_NullTelemetryClient() { + TelemetryLoggerMiddleware logger = new TelemetryLoggerMiddleware(null, true); + Assert.assertNotNull(logger.getTelemetryClient()); + } + + @Test + public void Telemetry_LogActivities() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new TelemetryLoggerMiddleware(mockTelemetryClient, true) + ); + + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + Activity activity = new Activity(ActivityTypes.TYPING); + activity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + turnContext.sendActivity(activity).join(); + turnContext.sendActivity("echo:" + turnContext.getActivity().getText()).join(); + return CompletableFuture.completedFuture(null); + })).send("foo").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:foo").send("bar").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:bar").startTest().join(); + + // verify BotTelemetryClient was invoked 6 times, and capture arguments. + verify(mockTelemetryClient, times(6)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + Assert.assertEquals(7, properties.get(0).size()); + Assert.assertTrue(properties.get(0).containsKey("fromId")); + Assert.assertTrue(properties.get(0).containsKey("conversationName")); + Assert.assertTrue(properties.get(0).containsKey("locale")); + Assert.assertTrue(properties.get(0).containsKey("recipientId")); + Assert.assertTrue(properties.get(0).containsKey("recipientName")); + Assert.assertTrue(properties.get(0).containsKey("fromName")); + Assert.assertTrue(properties.get(0).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(0).get("text"), "foo")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(1)); + Assert.assertEquals(5, properties.get(1).size()); + Assert.assertTrue(properties.get(1).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(1).containsKey("recipientId")); + Assert.assertTrue(properties.get(1).containsKey("conversationName")); + Assert.assertTrue(properties.get(1).containsKey("locale")); + Assert.assertTrue(properties.get(1).containsKey("recipientName")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(2)); + Assert.assertEquals(6, properties.get(2).size()); + Assert.assertTrue(properties.get(2).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(2).containsKey("recipientId")); + Assert.assertTrue(properties.get(2).containsKey("conversationName")); + Assert.assertTrue(properties.get(2).containsKey("locale")); + Assert.assertTrue(properties.get(2).containsKey("recipientName")); + Assert.assertTrue(properties.get(2).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(2).get("text"), "echo:foo")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(3)); + Assert.assertEquals(7, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("fromId")); + Assert.assertTrue(properties.get(3).containsKey("conversationName")); + Assert.assertTrue(properties.get(3).containsKey("locale")); + Assert.assertTrue(properties.get(3).containsKey("recipientId")); + Assert.assertTrue(properties.get(3).containsKey("recipientName")); + Assert.assertTrue(properties.get(3).containsKey("fromName")); + Assert.assertTrue(properties.get(3).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(3).get("text"), "bar")); + } + + @Test + public void Telemetry_NoPII() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new TelemetryLoggerMiddleware(mockTelemetryClient, false) + ); + + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + Activity activity = new Activity(ActivityTypes.TYPING); + activity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + turnContext.sendActivity(activity).join(); + turnContext.sendActivity("echo:" + turnContext.getActivity().getText()).join(); + return CompletableFuture.completedFuture(null); + })).send("foo").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:foo").send("bar").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:bar").startTest().join(); + + // verify BotTelemetryClient was invoked 6 times, and capture arguments. + verify(mockTelemetryClient, times(6)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + Assert.assertEquals(5, properties.get(0).size()); + Assert.assertTrue(properties.get(0).containsKey("fromId")); + Assert.assertTrue(properties.get(0).containsKey("conversationName")); + Assert.assertTrue(properties.get(0).containsKey("locale")); + Assert.assertTrue(properties.get(0).containsKey("recipientId")); + Assert.assertTrue(properties.get(0).containsKey("recipientName")); + Assert.assertFalse(properties.get(0).containsKey("fromName")); + Assert.assertFalse(properties.get(0).containsKey("text")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(1)); + Assert.assertEquals(4, properties.get(1).size()); + Assert.assertTrue(properties.get(1).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(1).containsKey("recipientId")); + Assert.assertTrue(properties.get(1).containsKey("conversationName")); + Assert.assertTrue(properties.get(1).containsKey("locale")); + Assert.assertFalse(properties.get(1).containsKey("recipientName")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(2)); + Assert.assertEquals(4, properties.get(2).size()); + Assert.assertTrue(properties.get(2).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(2).containsKey("recipientId")); + Assert.assertTrue(properties.get(2).containsKey("conversationName")); + Assert.assertTrue(properties.get(2).containsKey("locale")); + Assert.assertFalse(properties.get(2).containsKey("recipientName")); + Assert.assertFalse(properties.get(2).containsKey("text")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(3)); + Assert.assertEquals(5, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("fromId")); + Assert.assertTrue(properties.get(3).containsKey("conversationName")); + Assert.assertTrue(properties.get(3).containsKey("locale")); + Assert.assertTrue(properties.get(3).containsKey("recipientId")); + Assert.assertTrue(properties.get(3).containsKey("recipientName")); + Assert.assertFalse(properties.get(3).containsKey("fromName")); + Assert.assertFalse(properties.get(3).containsKey("text")); + } + + @Test + public void Transcript_LogUpdateActivities() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new TelemetryLoggerMiddleware(mockTelemetryClient, true) + ); + Activity[] activityToUpdate = new Activity[] { null }; + + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + + if (StringUtils.equals(turnContext.getActivity().getText(), "update")) { + activityToUpdate[0].setText("new response"); + turnContext.updateActivity(activityToUpdate[0]).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + ResourceResponse response = turnContext.sendActivity(activity).join(); + activity.setId(response.getId()); + activityToUpdate[0] = Activity.clone(activity); + } + + return CompletableFuture.completedFuture(null); + })).send("foo").send("update").assertReply("new response").startTest().join(); + + // verify BotTelemetryClient was invoked 4 times, and capture arguments. + verify(mockTelemetryClient, times(4)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGUPDATEEVENT, eventNames.get(3)); + Assert.assertEquals(5, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("recipientId")); + Assert.assertTrue(properties.get(3).containsKey("conversationId")); + Assert.assertTrue(properties.get(3).containsKey("conversationName")); + Assert.assertTrue(properties.get(3).containsKey("locale")); + Assert.assertTrue(properties.get(3).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(3).get("text"), "new response")); + } + + @Test + public void Transcript_LogDeleteActivities() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new TelemetryLoggerMiddleware(mockTelemetryClient, true) + ); + + String[] activityId = new String[] { null }; + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + + if (StringUtils.equals(turnContext.getActivity().getText(), "deleteIt")) { + turnContext.deleteActivity(activityId[0]).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + ResourceResponse response = turnContext.sendActivity(activity).join(); + activityId[0] = response.getId(); + } + + return CompletableFuture.completedFuture(null); + })).send("foo").assertReply("response").send("deleteIt").startTest().join(); + + // verify BotTelemetryClient was invoked 4 times, and capture arguments. + verify(mockTelemetryClient, times(4)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGDELETEEVENT, eventNames.get(3)); + Assert.assertEquals(3, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("recipientId")); + Assert.assertTrue(properties.get(3).containsKey("conversationId")); + Assert.assertTrue(properties.get(3).containsKey("conversationName")); + } + + @Test + public void Telemetry_OverrideReceive() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new OverrideReceiveLogger(mockTelemetryClient, true) + ); + + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + Activity activity = new Activity(ActivityTypes.TYPING); + activity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + turnContext.sendActivity(activity).join(); + turnContext.sendActivity("echo:" + turnContext.getActivity().getText()).join(); + return CompletableFuture.completedFuture(null); + })).send("foo").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:foo").send("bar").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:bar").startTest().join(); + + // verify BotTelemetryClient was invoked 8 times, and capture arguments. + verify(mockTelemetryClient, times(8)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + Assert.assertEquals(2, properties.get(0).size()); + Assert.assertTrue(properties.get(0).containsKey("foo")); + Assert.assertTrue(StringUtils.equals(properties.get(0).get("foo"), "bar")); + Assert.assertTrue(properties.get(0).containsKey("ImportantProperty")); + Assert.assertTrue( + StringUtils.equals(properties.get(0).get("ImportantProperty"), "ImportantValue") + ); + + Assert.assertEquals("MyReceive", eventNames.get(1)); + Assert.assertEquals(7, properties.get(1).size()); + Assert.assertTrue(properties.get(1).containsKey("fromId")); + Assert.assertTrue(properties.get(1).containsKey("conversationName")); + Assert.assertTrue(properties.get(1).containsKey("locale")); + Assert.assertTrue(properties.get(1).containsKey("recipientId")); + Assert.assertTrue(properties.get(1).containsKey("recipientName")); + Assert.assertTrue(properties.get(1).containsKey("fromName")); + Assert.assertTrue(properties.get(1).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(1).get("text"), "foo")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(2)); + Assert.assertEquals(5, properties.get(2).size()); + Assert.assertTrue(properties.get(2).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(2).containsKey("recipientId")); + Assert.assertTrue(properties.get(2).containsKey("conversationName")); + Assert.assertTrue(properties.get(2).containsKey("locale")); + Assert.assertTrue(properties.get(2).containsKey("recipientName")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(3)); + Assert.assertEquals(6, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(3).containsKey("recipientId")); + Assert.assertTrue(properties.get(3).containsKey("conversationName")); + Assert.assertTrue(properties.get(3).containsKey("locale")); + Assert.assertTrue(properties.get(3).containsKey("recipientName")); + Assert.assertTrue(properties.get(3).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(3).get("text"), "echo:foo")); + } + + @Test + public void Telemetry_OverrideSend() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new OverrideSendLogger(mockTelemetryClient, true) + ); + + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + Activity activity = new Activity(ActivityTypes.TYPING); + activity.setRelatesTo(turnContext.getActivity().getRelatesTo()); + turnContext.sendActivity(activity).join(); + turnContext.sendActivity("echo:" + turnContext.getActivity().getText()).join(); + return CompletableFuture.completedFuture(null); + })).send("foo").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:foo").send("bar").assertReply(activity -> { + Assert.assertEquals(activity.getType(), ActivityTypes.TYPING); + }).assertReply("echo:bar").startTest().join(); + + // verify BotTelemetryClient was invoked 10 times, and capture arguments. + verify(mockTelemetryClient, times(10)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + Assert.assertEquals(7, properties.get(0).size()); + Assert.assertTrue(properties.get(0).containsKey("fromId")); + Assert.assertTrue(properties.get(0).containsKey("conversationName")); + Assert.assertTrue(properties.get(0).containsKey("locale")); + Assert.assertTrue(properties.get(0).containsKey("recipientId")); + Assert.assertTrue(properties.get(0).containsKey("recipientName")); + Assert.assertTrue(properties.get(0).containsKey("fromName")); + Assert.assertTrue(properties.get(0).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(0).get("text"), "foo")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(1)); + Assert.assertEquals(2, properties.get(1).size()); + Assert.assertTrue(properties.get(1).containsKey("foo")); + Assert.assertTrue(StringUtils.equals(properties.get(1).get("foo"), "bar")); + Assert.assertTrue(properties.get(1).containsKey("ImportantProperty")); + Assert.assertTrue( + StringUtils.equals(properties.get(1).get("ImportantProperty"), "ImportantValue") + ); + + Assert.assertEquals("MySend", eventNames.get(2)); + Assert.assertEquals(5, properties.get(2).size()); + Assert.assertTrue(properties.get(2).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(2).containsKey("recipientId")); + Assert.assertTrue(properties.get(2).containsKey("conversationName")); + Assert.assertTrue(properties.get(2).containsKey("locale")); + Assert.assertTrue(properties.get(2).containsKey("recipientName")); + } + + @Test + public void Telemetry_OverrideUpdateDeleteActivities() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new OverrideUpdateDeleteLogger(mockTelemetryClient, true) + ); + + Activity[] activityToUpdate = new Activity[] { null }; + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + + if (StringUtils.equals(turnContext.getActivity().getText(), "update")) { + activityToUpdate[0].setText("new response"); + turnContext.updateActivity(activityToUpdate[0]).join(); + turnContext.deleteActivity(turnContext.getActivity().getId()).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + ResourceResponse response = turnContext.sendActivity(activity).join(); + activity.setId(response.getId()); + + activityToUpdate[0] = Activity.clone(activity); + } + + return CompletableFuture.completedFuture(null); + })).send("foo").send("update").assertReply("new response").startTest().join(); + + // verify BotTelemetryClient was invoked 5 times, and capture arguments. + verify(mockTelemetryClient, times(5)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGUPDATEEVENT, eventNames.get(3)); + Assert.assertEquals(2, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("foo")); + Assert.assertTrue(StringUtils.equals(properties.get(3).get("foo"), "bar")); + Assert.assertTrue(properties.get(3).containsKey("ImportantProperty")); + Assert.assertTrue( + StringUtils.equals(properties.get(3).get("ImportantProperty"), "ImportantValue") + ); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGDELETEEVENT, eventNames.get(4)); + Assert.assertEquals(2, properties.get(4).size()); + Assert.assertTrue(properties.get(4).containsKey("foo")); + Assert.assertTrue(StringUtils.equals(properties.get(4).get("foo"), "bar")); + Assert.assertTrue(properties.get(4).containsKey("ImportantProperty")); + Assert.assertTrue( + StringUtils.equals(properties.get(4).get("ImportantProperty"), "ImportantValue") + ); + } + + @Test + public void Telemetry_AdditionalProps() { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter().use( + new OverrideFillLogger(mockTelemetryClient, true) + ); + + Activity[] activityToUpdate = new Activity[] { null }; + String[] conversationId = new String[] { null }; + new TestFlow(adapter, (turnContext -> { + conversationId[0] = turnContext.getActivity().getConversation().getId(); + + if (StringUtils.equals(turnContext.getActivity().getText(), "update")) { + activityToUpdate[0].setText("new response"); + turnContext.updateActivity(activityToUpdate[0]).join(); + turnContext.deleteActivity(turnContext.getActivity().getId()).join(); + } else { + Activity activity = turnContext.getActivity().createReply("response"); + ResourceResponse response = turnContext.sendActivity(activity).join(); + activity.setId(response.getId()); + + activityToUpdate[0] = Activity.clone(activity); + } + + return CompletableFuture.completedFuture(null); + })).send("foo").send("update").assertReply("new response").startTest().join(); + + // verify BotTelemetryClient was invoked 5 times, and capture arguments. + verify(mockTelemetryClient, times(5)).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + Assert.assertEquals(9, properties.get(0).size()); + Assert.assertTrue(properties.get(0).containsKey("fromId")); + Assert.assertTrue(properties.get(0).containsKey("conversationName")); + Assert.assertTrue(properties.get(0).containsKey("locale")); + Assert.assertTrue(properties.get(0).containsKey("recipientId")); + Assert.assertTrue(properties.get(0).containsKey("recipientName")); + Assert.assertTrue(properties.get(0).containsKey("fromName")); + Assert.assertTrue(properties.get(0).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(0).get("text"), "foo")); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGSENDEVENT, eventNames.get(1)); + Assert.assertEquals(8, properties.get(1).size()); + Assert.assertTrue(properties.get(1).containsKey("foo")); + Assert.assertTrue(properties.get(1).containsKey("replyActivityId")); + Assert.assertTrue(properties.get(1).containsKey("recipientId")); + Assert.assertTrue(properties.get(1).containsKey("conversationName")); + Assert.assertTrue(properties.get(1).containsKey("locale")); + Assert.assertTrue(properties.get(1).containsKey("foo")); + Assert.assertTrue(properties.get(1).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(1).get("text"), "response")); + Assert.assertTrue(StringUtils.equals(properties.get(1).get("foo"), "bar")); + Assert.assertTrue( + StringUtils.equals(properties.get(1).get("ImportantProperty"), "ImportantValue") + ); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGUPDATEEVENT, eventNames.get(3)); + Assert.assertEquals(7, properties.get(3).size()); + Assert.assertTrue(properties.get(3).containsKey("conversationId")); + Assert.assertTrue(properties.get(3).containsKey("conversationName")); + Assert.assertTrue(properties.get(3).containsKey("locale")); + Assert.assertTrue(properties.get(3).containsKey("foo")); + Assert.assertTrue(properties.get(3).containsKey("text")); + Assert.assertTrue(StringUtils.equals(properties.get(3).get("text"), "new response")); + Assert.assertTrue(StringUtils.equals(properties.get(3).get("foo"), "bar")); + Assert.assertTrue( + StringUtils.equals(properties.get(3).get("ImportantProperty"), "ImportantValue") + ); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGDELETEEVENT, eventNames.get(4)); + Assert.assertEquals(5, properties.get(4).size()); + Assert.assertTrue(properties.get(4).containsKey("recipientId")); + Assert.assertTrue(properties.get(4).containsKey("conversationName")); + Assert.assertTrue(properties.get(4).containsKey("conversationId")); + Assert.assertTrue(properties.get(4).containsKey("foo")); + Assert.assertTrue(StringUtils.equals(properties.get(4).get("foo"), "bar")); + Assert.assertTrue( + StringUtils.equals(properties.get(4).get("ImportantProperty"), "ImportantValue") + ); + } + + @Test + public void Telemetry_LogAttachments() throws JsonProcessingException { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter(Channels.MSTEAMS).use( + new TelemetryLoggerMiddleware(mockTelemetryClient, true) + ); + + TeamInfo teamInfo = new TeamInfo(); + teamInfo.setId("teamId"); + teamInfo.setName("teamName"); + + Activity activity = MessageFactory.text("test"); + ChannelAccount from = new ChannelAccount(); + from.setId("userId"); + from.setName("userName"); + from.setAadObjectId("aadId"); + activity.setFrom(from); + Attachment attachment = new Attachment(); + attachment.setContent("Hello World"); + attachment.setContentType("test/attachment"); + attachment.setName("testname"); + activity.setAttachment(attachment); + + new TestFlow(adapter).send(activity).startTest().join(); + + verify(mockTelemetryClient).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + String loggedAttachment = properties.get(0).get("attachments"); + String originalAttachment = Serialization.toString(activity.getAttachments()); + Assert.assertTrue(StringUtils.equals(loggedAttachment, originalAttachment)); + } + + + @Test + public void Telemetry_LogTeamsProperties() throws JsonProcessingException { + BotTelemetryClient mockTelemetryClient = mock(BotTelemetryClient.class); + TestAdapter adapter = new TestAdapter(Channels.MSTEAMS).use( + new TelemetryLoggerMiddleware(mockTelemetryClient, true) + ); + + TeamInfo teamInfo = new TeamInfo(); + teamInfo.setId("teamId"); + teamInfo.setName("teamName"); + + TeamsChannelData channelData = new TeamsChannelData(); + channelData.setTeam(teamInfo); + TenantInfo tenant = new TenantInfo(); + tenant.setId("tenantId"); + channelData.setTenant(tenant); + + Activity activity = MessageFactory.text("test"); + activity.setChannelData(channelData); + ChannelAccount from = new ChannelAccount(); + from.setId("userId"); + from.setName("userName"); + from.setAadObjectId("aadId"); + activity.setFrom(from); + + new TestFlow(adapter).send(activity).startTest().join(); + + verify(mockTelemetryClient).trackEvent( + eventNameCaptor.capture(), + propertiesCaptor.capture() + ); + List eventNames = eventNameCaptor.getAllValues(); + List> properties = propertiesCaptor.getAllValues(); + + Assert.assertEquals(TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, eventNames.get(0)); + + Assert.assertTrue(StringUtils.equals(properties.get(0).get("TeamsUserAadObjectId"), "aadId")); + Assert.assertTrue(StringUtils.equals(properties.get(0).get("TeamsTenantId"), "tenantId")); + Assert.assertTrue(StringUtils.equals(properties.get(0).get("TeamsTeamInfo"), Serialization.toString(teamInfo))); + } + + private static class OverrideReceiveLogger extends TelemetryLoggerMiddleware { + public OverrideReceiveLogger( + BotTelemetryClient withTelemetryClient, + boolean withLogPersonalInformation + ) { + super(withTelemetryClient, withLogPersonalInformation); + } + + @Override + protected CompletableFuture onReceiveActivity(Activity activity) { + Map customProperties = new HashMap(); + customProperties.put("foo", "bar"); + customProperties.put("ImportantProperty", "ImportantValue"); + + getTelemetryClient().trackEvent( + TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, + customProperties + ); + + return fillReceiveEventProperties(activity, null).thenApply(eventProperties -> { + getTelemetryClient().trackEvent("MyReceive", eventProperties); + return null; + }); + } + } + + private static class OverrideSendLogger extends TelemetryLoggerMiddleware { + public OverrideSendLogger( + BotTelemetryClient withTelemetryClient, + boolean withLogPersonalInformation + ) { + super(withTelemetryClient, withLogPersonalInformation); + } + + @Override + protected CompletableFuture onSendActivity(Activity activity) { + Map customProperties = new HashMap(); + customProperties.put("foo", "bar"); + customProperties.put("ImportantProperty", "ImportantValue"); + + getTelemetryClient().trackEvent( + TelemetryLoggerConstants.BOTMSGSENDEVENT, + customProperties + ); + + return fillSendEventProperties(activity, null).thenApply(eventProperties -> { + getTelemetryClient().trackEvent("MySend", eventProperties); + return null; + }); + } + } + + private static class OverrideUpdateDeleteLogger extends TelemetryLoggerMiddleware { + public OverrideUpdateDeleteLogger( + BotTelemetryClient withTelemetryClient, + boolean withLogPersonalInformation + ) { + super(withTelemetryClient, withLogPersonalInformation); + } + + @Override + protected CompletableFuture onUpdateActivity(Activity activity) { + Map properties = new HashMap(); + properties.put("foo", "bar"); + properties.put("ImportantProperty", "ImportantValue"); + + getTelemetryClient().trackEvent(TelemetryLoggerConstants.BOTMSGUPDATEEVENT, properties); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onDeleteActivity(Activity activity) { + Map properties = new HashMap(); + properties.put("foo", "bar"); + properties.put("ImportantProperty", "ImportantValue"); + + getTelemetryClient().trackEvent(TelemetryLoggerConstants.BOTMSGDELETEEVENT, properties); + return CompletableFuture.completedFuture(null); + } + } + + private static class OverrideFillLogger extends TelemetryLoggerMiddleware { + public OverrideFillLogger( + BotTelemetryClient withTelemetryClient, + boolean withLogPersonalInformation + ) { + super(withTelemetryClient, withLogPersonalInformation); + } + + @Override + protected CompletableFuture onReceiveActivity(Activity activity) { + Map customProperties = new HashMap(); + customProperties.put("foo", "bar"); + customProperties.put("ImportantProperty", "ImportantValue"); + + return fillReceiveEventProperties(activity, customProperties).thenApply( + allProperties -> { + getTelemetryClient().trackEvent( + TelemetryLoggerConstants.BOTMSGRECEIVEEVENT, + allProperties + ); + return null; + } + ); + } + + @Override + protected CompletableFuture onSendActivity(Activity activity) { + Map customProperties = new HashMap(); + customProperties.put("foo", "bar"); + customProperties.put("ImportantProperty", "ImportantValue"); + + return fillSendEventProperties(activity, customProperties).thenApply(allProperties -> { + getTelemetryClient().trackEvent( + TelemetryLoggerConstants.BOTMSGSENDEVENT, + allProperties + ); + return null; + }); + } + + @Override + protected CompletableFuture onUpdateActivity(Activity activity) { + Map customProperties = new HashMap(); + customProperties.put("foo", "bar"); + customProperties.put("ImportantProperty", "ImportantValue"); + + return fillUpdateEventProperties(activity, customProperties).thenApply( + allProperties -> { + getTelemetryClient().trackEvent( + TelemetryLoggerConstants.BOTMSGUPDATEEVENT, + allProperties + ); + return null; + } + ); + } + + @Override + protected CompletableFuture onDeleteActivity(Activity activity) { + Map customProperties = new HashMap(); + customProperties.put("foo", "bar"); + customProperties.put("ImportantProperty", "ImportantValue"); + + return fillDeleteEventProperties(activity, customProperties).thenApply( + allProperties -> { + getTelemetryClient().trackEvent( + TelemetryLoggerConstants.BOTMSGDELETEEVENT, + allProperties + ); + return null; + } + ); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestAdapterTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestAdapterTests.java new file mode 100644 index 000000000..12fae4aaa --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestAdapterTests.java @@ -0,0 +1,426 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class TestAdapterTests { + public CompletableFuture myBotLogic(TurnContext turnContext) { + switch (turnContext.getActivity().getText()) { + case "count": + turnContext.sendActivity(turnContext.getActivity().createReply("one")).join(); + turnContext.sendActivity(turnContext.getActivity().createReply("two")).join(); + turnContext.sendActivity(turnContext.getActivity().createReply("three")).join(); + break; + + case "ignore": + break; + + default: + turnContext.sendActivity( + turnContext.getActivity().createReply( + "echo:" + turnContext.getActivity().getText() + ) + ).join(); + break; + } + + return CompletableFuture.completedFuture(null); + } + + @Test + public void TestAdapter_ExceptionTypesOnTest() { + String uniqueExceptionId = UUID.randomUUID().toString(); + TestAdapter adapter = new TestAdapter(); + + try { + + new TestFlow(adapter, turnContext -> { + turnContext.sendActivity(turnContext.getActivity().createReply("one")).join(); + return CompletableFuture.completedFuture(null); + }).test("foo", activity -> { + throw new RuntimeException(uniqueExceptionId); + }).startTest().join(); + + Assert.fail("An exception should have been thrown"); + } catch (Throwable t) { + Assert.assertTrue(t.getMessage().contains(uniqueExceptionId)); + } + } + + @Test + public void TestAdapter_ExceptionInBotOnReceive() { + String uniqueExceptionId = UUID.randomUUID().toString(); + TestAdapter adapter = new TestAdapter(); + + try { + + new TestFlow(adapter, turnContext -> { + return Async.completeExceptionally(new RuntimeException(uniqueExceptionId)); + }).test("foo", activity -> { + Assert.assertNull(activity); + }).startTest().join(); + + Assert.fail("An exception should have been thrown"); + } catch (Throwable t) { + Assert.assertTrue(t.getMessage().contains(uniqueExceptionId)); + } + } + + @Test + public void TestAdapter_ExceptionTypesOnAssertReply() { + String uniqueExceptionId = UUID.randomUUID().toString(); + TestAdapter adapter = new TestAdapter(); + + try { + + new TestFlow(adapter, turnContext -> { + turnContext.sendActivity(turnContext.getActivity().createReply("one")).join(); + return CompletableFuture.completedFuture(null); + }).send("foo").assertReply(activity -> { + throw new RuntimeException(uniqueExceptionId); + }).startTest().join(); + + Assert.fail("An exception should have been thrown"); + } catch (Throwable t) { + Assert.assertTrue(t.getMessage().contains(uniqueExceptionId)); + } + } + + @Test + public void TestAdapter_SaySimple() { + TestAdapter adapter = new TestAdapter(); + new TestFlow(adapter, this::myBotLogic).test( + "foo", + "echo:foo", + "say with string works" + ).startTest().join(); + } + + @Test + public void TestAdapter_Say() { + TestAdapter adapter = new TestAdapter(); + Activity messageActivity = new Activity(ActivityTypes.MESSAGE); + messageActivity.setText("echo:foo"); + new TestFlow(adapter, this::myBotLogic).test( + "foo", + "echo:foo", + "say with string works" + ).test("foo", messageActivity, "say with activity works").test("foo", activity -> { + Assert.assertEquals("echo:foo", activity.getText()); + }, "say with validator works").startTest().join(); + } + + @Test + public void TestAdapter_SendReply() { + TestAdapter adapter = new TestAdapter(); + Activity messageActivity = new Activity(ActivityTypes.MESSAGE); + messageActivity.setText("echo:foo"); + new TestFlow(adapter, this::myBotLogic).send("foo").assertReply( + "echo:foo", + "say with string works" + ).send("foo").assertReply(messageActivity, "say with activity works").send("foo").assertReply(activity -> { + Assert.assertEquals("echo:foo", activity.getText()); + }, "say with validator works").startTest().join(); + } + + @Test + public void TestAdapter_ReplyOneOf() { + TestAdapter adapter = new TestAdapter(); + new TestFlow(adapter, this::myBotLogic).send("foo").assertReplyOneOf( + new String[] { "echo:bar", "echo:foo", "echo:blat" }, + "say with string works" + ).startTest().join(); + } + + @Test + public void TestAdapter_MultipleReplies() { + TestAdapter adapter = new TestAdapter(); + new TestFlow(adapter, this::myBotLogic).send("foo").assertReply("echo:foo").send( + "bar" + ).assertReply("echo:bar").send("ignore").send("count").assertReply("one").assertReply( + "two" + ).assertReply("three").startTest().join(); + } + + @Test + public void TestAdapter_TestFlow() { + String uniqueExceptionId = UUID.randomUUID().toString(); + TestAdapter adapter = new TestAdapter(); + + TestFlow testFlow = new TestFlow(adapter, turnContext -> { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new Exception()); + return result; + }).send("foo"); + + testFlow.startTest().exceptionally(exception -> { + Assert.assertTrue(exception instanceof CompletionException); + Assert.assertNotNull(exception.getCause()); + return null; + }).join(); + } + + @Test + public void TestAdapter_GetUserTokenAsyncReturnsNull() { + TestAdapter adapter = new TestAdapter(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId("directline"); + ChannelAccount from = new ChannelAccount(); + from.setId("testuser"); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + TokenResponse token = adapter.getUserToken(turnContext, "myconnection", null).join(); + Assert.assertNull(token); + } + + @Test + public void TestAdapter_GetUserTokenAsyncReturnsNullWithCode() { + TestAdapter adapter = new TestAdapter(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId("directline"); + ChannelAccount from = new ChannelAccount(); + from.setId("testuser"); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + TokenResponse token = adapter.getUserToken(turnContext, "myconnection", "abc123").join(); + Assert.assertNull(token); + } + + @Test + public void TestAdapter_GetUserTokenAsyncReturnsToken() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + String token = "abc123"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.addUserToken(connectionName, channelId, userId, token, null); + + TokenResponse tokenResponse = adapter.getUserToken( + turnContext, + connectionName, + null + ).join(); + Assert.assertNotNull(tokenResponse); + Assert.assertEquals(token, tokenResponse.getToken()); + Assert.assertEquals(connectionName, tokenResponse.getConnectionName()); + } + + @Test + public void TestAdapter_GetUserTokenAsyncReturnsTokenWithMagicCode() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + String token = "abc123"; + String magicCode = "888999"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.addUserToken(connectionName, channelId, userId, token, magicCode); + + TokenResponse tokenResponse = adapter.getUserToken( + turnContext, + connectionName, + null + ).join(); + Assert.assertNull(tokenResponse); + + tokenResponse = adapter.getUserToken(turnContext, connectionName, magicCode).join(); + Assert.assertNotNull(tokenResponse); + Assert.assertEquals(token, tokenResponse.getToken()); + Assert.assertEquals(connectionName, tokenResponse.getConnectionName()); + } + + @Test + public void TestAdapter_GetSignInLink() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + String link = adapter.getOAuthSignInLink(turnContext, connectionName, userId, null).join(); + Assert.assertNotNull(link); + Assert.assertTrue(link.length() > 0); + } + + @Test + public void TestAdapter_GetSignInLinkWithNoUserId() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + String link = adapter.getOAuthSignInLink(turnContext, connectionName).join(); + Assert.assertNotNull(link); + Assert.assertTrue(link.length() > 0); + } + + @Test + public void TestAdapter_SignOutNoop() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.signOutUser(turnContext, null, null).join(); + adapter.signOutUser(turnContext, connectionName, null).join(); + adapter.signOutUser(turnContext, connectionName, userId).join(); + adapter.signOutUser(turnContext, null, userId).join(); + } + + @Test + public void TestAdapter_SignOut() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + String token = "abc123"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.addUserToken(connectionName, channelId, userId, token, null); + + TokenResponse tokenResponse = adapter.getUserToken( + turnContext, + connectionName, + null + ).join(); + Assert.assertNotNull(tokenResponse); + Assert.assertEquals(token, tokenResponse.getToken()); + Assert.assertEquals(connectionName, tokenResponse.getConnectionName()); + + adapter.signOutUser(turnContext, connectionName, userId).join(); + tokenResponse = adapter.getUserToken(turnContext, connectionName, null).join(); + Assert.assertNull(tokenResponse); + } + + @Test + public void TestAdapter_SignOutAll() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + String token = "abc123"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.addUserToken("ABC", channelId, userId, token, null); + adapter.addUserToken("DEF", channelId, userId, token, null); + + TokenResponse tokenResponse = adapter.getUserToken(turnContext, "ABC", null).join(); + Assert.assertNotNull(tokenResponse); + Assert.assertEquals(token, tokenResponse.getToken()); + Assert.assertEquals("ABC", tokenResponse.getConnectionName()); + + tokenResponse = adapter.getUserToken(turnContext, "DEF", null).join(); + Assert.assertNotNull(tokenResponse); + Assert.assertEquals(token, tokenResponse.getToken()); + Assert.assertEquals("DEF", tokenResponse.getConnectionName()); + + adapter.signOutUser(turnContext, null, userId).join(); + tokenResponse = adapter.getUserToken(turnContext, "ABC", null).join(); + Assert.assertNull(tokenResponse); + tokenResponse = adapter.getUserToken(turnContext, "DEF", null).join(); + Assert.assertNull(tokenResponse); + } + + @Test + public void TestAdapter_GetTokenStatus() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + String token = "abc123"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.addUserToken("ABC", channelId, userId, token, null); + adapter.addUserToken("DEF", channelId, userId, token, null); + + List status = adapter.getTokenStatus(turnContext, userId, null).join(); + Assert.assertNotNull(status); + Assert.assertEquals(2, status.size()); + } + + @Test + public void TestAdapter_GetTokenStatusWithFilter() { + TestAdapter adapter = new TestAdapter(); + String connectionName = "myConnection"; + String channelId = "directline"; + String userId = "testUser"; + String token = "abc123"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId(channelId); + ChannelAccount from = new ChannelAccount(); + from.setId(userId); + activity.setFrom(from); + TurnContext turnContext = new TurnContextImpl(adapter, activity); + + adapter.addUserToken("ABC", channelId, userId, token, null); + adapter.addUserToken("DEF", channelId, userId, token, null); + + List status = adapter.getTokenStatus(turnContext, userId, "DEF").join(); + Assert.assertNotNull(status); + Assert.assertEquals(1, status.size()); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestMessage.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestMessage.java new file mode 100644 index 000000000..8d3f8a2d0 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestMessage.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; + +public class TestMessage { + public static Activity Message() { + return TestMessage.Message("1234"); + } + + public static Activity Message(String id) { + Activity a = new Activity(ActivityTypes.MESSAGE); + a.setId(id); + a.setText("test"); + a.setFrom(new ChannelAccount("user", "User Name")); + a.setRecipient(new ChannelAccount("bot", "Bot Name")); + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId("convo"); + conversationAccount.setName("Convo Name"); + a.setConversation(conversationAccount); + a.setChannelId("UnitTest"); + a.setServiceUrl("https://example.org"); + return a; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestPocoState.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestPocoState.java new file mode 100644 index 000000000..e17d40e3d --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestPocoState.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +public class TestPocoState { + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + private String value; +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestUtilities.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestUtilities.java new file mode 100644 index 000000000..87fdd7d54 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TestUtilities.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; + +public final class TestUtilities { + public static TurnContext createEmptyContext() { + TestAdapter adapter = new TestAdapter(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId("EmptyContext"); + ConversationAccount conversation = new ConversationAccount(); + conversation.setId("test"); + activity.setConversation(conversation); + ChannelAccount from = new ChannelAccount(); + from.setId("empty@empty.context.org"); + activity.setFrom(from); + + return new TurnContextImpl(adapter, activity); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TranscriptBaseTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TranscriptBaseTests.java new file mode 100644 index 000000000..ce3581070 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TranscriptBaseTests.java @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import java.util.concurrent.CompletionException; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class TranscriptBaseTests { + protected TranscriptStore store; + private ObjectMapper mapper; + + protected TranscriptBaseTests() { + mapper = new ObjectMapper(); + mapper.findAndRegisterModules(); + } + + protected void BadArgs() { + try { + store.logActivity(null).join(); + Assert.fail("logActivity Should have thrown on null"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof IllegalArgumentException); + } catch (Throwable t) { + Assert.fail("logActivity Should have thrown ArgumentNull exception on null"); + } + + try { + store.getTranscriptActivities(null, null).join(); + Assert.fail("getTranscriptActivities Should have thrown on null"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof IllegalArgumentException); + } catch (Throwable t) { + Assert.fail( + "getTranscriptActivities Should have thrown ArgumentNull exception on null" + ); + } + + try { + store.getTranscriptActivities("asdfds", null).join(); + Assert.fail("getTranscriptActivities Should have thrown on null"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof IllegalArgumentException); + } catch (Throwable t) { + Assert.fail( + "getTranscriptActivities Should have thrown ArgumentNull exception on null" + ); + } + + try { + store.listTranscripts(null).join(); + Assert.fail("listTranscripts Should have thrown on null"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof IllegalArgumentException); + } catch (Throwable t) { + Assert.fail("listTranscripts Should have thrown ArgumentNull exception on null"); + } + + try { + store.deleteTranscript(null, null).join(); + Assert.fail("deleteTranscript Should have thrown on null channelId"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof IllegalArgumentException); + } catch (Throwable t) { + Assert.fail( + "deleteTranscript Should have thrown ArgumentNull exception on null channelId" + ); + } + + try { + store.deleteTranscript("test", null).join(); + Assert.fail("deleteTranscript Should have thrown on null conversationId"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof IllegalArgumentException); + } catch (Throwable t) { + Assert.fail( + "deleteTranscript Should have thrown ArgumentNull exception on null conversationId" + ); + } + } + + protected void LogActivity() { + String conversationId = "_LogActivity"; + List activities = createActivities( + conversationId, + OffsetDateTime.now(ZoneId.of("UTC")) + ); + Activity activity = activities.get(0); + store.logActivity(activity).join(); + + PagedResult results = store.getTranscriptActivities( + "test", + conversationId + ).join(); + Assert.assertEquals(1, results.getItems().size()); + + String src; + String transcript; + try { + src = mapper.writeValueAsString(activity); + transcript = mapper.writeValueAsString(results.getItems().get(0)); + } catch (Throwable t) { + Assert.fail("Throw during json compare"); + return; + } + + Assert.assertEquals(src, transcript); + } + + protected void LogMultipleActivities() { + String conversationId = "LogMultipleActivities"; + OffsetDateTime start = OffsetDateTime.now(ZoneId.of("UTC")); + List activities = createActivities(conversationId, start); + + for (Activity activity : activities) { + store.logActivity(activity).join(); + } + + // make sure other channels and conversations don't return results + PagedResult pagedResult = store.getTranscriptActivities( + "bogus", + conversationId + ).join(); + Assert.assertNull(pagedResult.getContinuationToken()); + Assert.assertEquals(0, pagedResult.getItems().size()); + + // make sure other channels and conversations don't return results + pagedResult = store.getTranscriptActivities("test", "bogus").join(); + Assert.assertNull(pagedResult.getContinuationToken()); + Assert.assertEquals(0, pagedResult.getItems().size()); + + // make sure the original and transcript result activities are the same + int indexActivity = 0; + for (Activity result : pagedResult.getItems().stream().sorted( + Comparator.comparing(Activity::getTimestamp) + ).collect(Collectors.toList())) { + String src; + String transcript; + try { + src = mapper.writeValueAsString(activities.get(indexActivity++)); + transcript = mapper.writeValueAsString(result); + } catch (Throwable t) { + Assert.fail("Throw during json compare"); + return; + } + + Assert.assertEquals(src, transcript); + } + + pagedResult = store.getTranscriptActivities( + "test", + conversationId, + null, + start.plusMinutes(5) + ).join(); + Assert.assertEquals(activities.size() / 2, pagedResult.getItems().size()); + + // make sure the original and transcript result activities are the same + indexActivity = 5; + for (Activity result : pagedResult.getItems().stream().sorted( + Comparator.comparing(Activity::getTimestamp) + ).collect(Collectors.toList())) { + String src; + String transcript; + try { + src = mapper.writeValueAsString(activities.get(indexActivity++)); + transcript = mapper.writeValueAsString(result); + } catch (Throwable t) { + Assert.fail("Throw during json compare"); + return; + } + + Assert.assertEquals(src, transcript); + } + } + + protected void DeleteTranscript() { + String conversationId = "_DeleteConversation"; + OffsetDateTime start = OffsetDateTime.now(ZoneId.of("UTC")); + List activities = createActivities(conversationId, start); + activities.forEach(a -> store.logActivity(a).join()); + + String conversationId2 = "_DeleteConversation2"; + start = OffsetDateTime.now(ZoneId.of("UTC")); + List activities2 = createActivities(conversationId2, start); + activities2.forEach(a -> store.logActivity(a).join()); + + PagedResult pagedResult = store.getTranscriptActivities( + "test", + conversationId + ).join(); + PagedResult pagedResult2 = store.getTranscriptActivities( + "test", + conversationId2 + ).join(); + + Assert.assertEquals(activities.size(), pagedResult.getItems().size()); + Assert.assertEquals(activities2.size(), pagedResult2.getItems().size()); + + store.deleteTranscript("test", conversationId).join(); + + pagedResult = store.getTranscriptActivities("test", conversationId).join(); + pagedResult2 = store.getTranscriptActivities("test", conversationId2).join(); + + Assert.assertEquals(0, pagedResult.getItems().size()); + Assert.assertEquals(activities.size(), pagedResult2.getItems().size()); + } + + protected void GetTranscriptActivities() { + String conversationId = "_GetConversationActivitiesPaging"; + OffsetDateTime start = OffsetDateTime.now(ZoneId.of("UTC")); + List activities = createActivities(conversationId, start, 50); + + // log in parallel batches of 10 + int[] pos = new int[] { 0 }; + for (List group : activities.stream().collect( + Collectors.groupingBy(a -> pos[0]++ / 10) + ).values()) { + group.stream().map(a -> store.logActivity(a)).collect( + CompletableFutures.toFutureList() + ).join(); + } + + Set seen = new HashSet<>(); + PagedResult pagedResult = null; + int pageSize = 0; + do { + pagedResult = store.getTranscriptActivities( + "test", + conversationId, + pagedResult != null ? pagedResult.getContinuationToken() : null + ).join(); + Assert.assertNotNull(pagedResult); + Assert.assertNotNull(pagedResult.getItems()); + + // NOTE: Assumes page size is consistent + if (pageSize == 0) { + pageSize = pagedResult.getItems().size(); + } else if (pageSize == pagedResult.getItems().size()) { + Assert.assertTrue(!StringUtils.isEmpty(pagedResult.getContinuationToken())); + } + + for (Activity item : pagedResult.getItems()) { + Assert.assertFalse(seen.contains(item.getId())); + seen.add(item.getId()); + } + } while (pagedResult.getContinuationToken() != null); + + Assert.assertEquals(activities.size(), seen.size()); + } + + protected void GetTranscriptActivitiesStartDate() { + String conversationId = "_GetConversationActivitiesStartDate"; + OffsetDateTime start = OffsetDateTime.now(ZoneId.of("UTC")); + List activities = createActivities(conversationId, start, 50); + + // log in parallel batches of 10 + int[] pos = new int[] { 0 }; + for (List group : activities.stream().collect( + Collectors.groupingBy(a -> pos[0]++ / 10) + ).values()) { + group.stream().map(a -> store.logActivity(a)).collect( + CompletableFutures.toFutureList() + ).join(); + } + + Set seen = new HashSet<>(); + OffsetDateTime startDate = start.plusMinutes(50); + PagedResult pagedResult = null; + int pageSize = 0; + do { + pagedResult = store.getTranscriptActivities( + "test", + conversationId, + pagedResult != null ? pagedResult.getContinuationToken() : null, + startDate + ).join(); + Assert.assertNotNull(pagedResult); + Assert.assertNotNull(pagedResult.getItems()); + + // NOTE: Assumes page size is consistent + if (pageSize == 0) { + pageSize = pagedResult.getItems().size(); + } else if (pageSize == pagedResult.getItems().size()) { + Assert.assertTrue(!StringUtils.isEmpty(pagedResult.getContinuationToken())); + } + + for (Activity item : pagedResult.getItems()) { + Assert.assertFalse(seen.contains(item.getId())); + seen.add(item.getId()); + } + } while (pagedResult.getContinuationToken() != null); + + Assert.assertEquals(activities.size() / 2, seen.size()); + + for (Activity a : activities.stream().filter( + a -> a.getTimestamp().compareTo(startDate) >= 0 + ).collect(Collectors.toList())) { + Assert.assertTrue(seen.contains(a.getId())); + } + + for (Activity a : activities.stream().filter( + a -> a.getTimestamp().compareTo(startDate) < 0 + ).collect(Collectors.toList())) { + Assert.assertFalse(seen.contains(a.getId())); + } + } + + protected void ListTranscripts() { + OffsetDateTime start = OffsetDateTime.now(ZoneId.of("UTC")); + + List conversationIds = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + conversationIds.add("_ListConversations" + i); + } + + List activities = new ArrayList<>(); + for (String conversationId : conversationIds) { + activities.addAll(createActivities(conversationId, start, 1)); + } + + // log in parallel batches of 10 + int[] pos = new int[] { 0 }; + for (List group : activities.stream().collect( + Collectors.groupingBy(a -> pos[0]++ / 10) + ).values()) { + group.stream().map(a -> store.logActivity(a)).collect( + CompletableFutures.toFutureList() + ).join(); + } + + Set seen = new HashSet<>(); + PagedResult pagedResult = null; + int pageSize = 0; + do { + pagedResult = store.listTranscripts( + "test", + pagedResult != null ? pagedResult.getContinuationToken() : null + ).join(); + Assert.assertNotNull(pagedResult); + Assert.assertNotNull(pagedResult.getItems()); + + // NOTE: Assumes page size is consistent + if (pageSize == 0) { + pageSize = pagedResult.getItems().size(); + } else if (pageSize == pagedResult.getItems().size()) { + Assert.assertTrue(!StringUtils.isEmpty(pagedResult.getContinuationToken())); + } + + for (TranscriptInfo item : pagedResult.getItems()) { + Assert.assertFalse(seen.contains(item.getId())); + seen.add(item.getId()); + } + } while (pagedResult.getContinuationToken() != null); + + Assert.assertEquals(conversationIds.size(), seen.size()); + Assert.assertTrue(conversationIds.stream().allMatch(seen::contains)); + } + + private List createActivities(String conversationId, OffsetDateTime ts) { + return createActivities(conversationId, ts, 5); + } + + private List createActivities(String conversationId, OffsetDateTime ts, int count) { + List activities = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setTimestamp(ts); + activity.setId(UUID.randomUUID().toString()); + activity.setText(Integer.toString(i)); + activity.setChannelId("test"); + activity.setFrom(new ChannelAccount("User" + i)); + activity.setConversation(new ConversationAccount(conversationId)); + activity.setRecipient(new ChannelAccount("Bot1", "2")); + activity.setServiceUrl("http://foo.com/api/messages"); + activities.add(activity); + ts = ts.plusMinutes(1); + + activity = new Activity(ActivityTypes.MESSAGE); + activity.setTimestamp(ts); + activity.setId(UUID.randomUUID().toString()); + activity.setText(Integer.toString(i)); + activity.setChannelId("test"); + activity.setFrom(new ChannelAccount("Bot1", "2")); + activity.setConversation(new ConversationAccount(conversationId)); + activity.setRecipient(new ChannelAccount("User" + i)); + activity.setServiceUrl("http://foo.com/api/messages"); + activities.add(activity); + ts = ts.plusMinutes(1); + } + ; + + return activities; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TranscriptMiddlewareTest.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TranscriptMiddlewareTest.java new file mode 100644 index 000000000..4fbd98a13 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TranscriptMiddlewareTest.java @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.schema.*; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.concurrent.CompletableFuture; + +public class TranscriptMiddlewareTest { + + @Test + public final void Transcript_MiddlewareTest() { + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TranscriptLoggerMiddleware logger = new TranscriptLoggerMiddleware(transcriptStore); + TestAdapter adapter = new TestAdapter(); + Activity activity = Activity.createMessageActivity(); + activity.setFrom(new ChannelAccount("acctid", "MyAccount", RoleTypes.USER)); + TurnContextImpl context = new TurnContextImpl(adapter, activity); + NextDelegate nd = new NextDelegate() { + @Override + public CompletableFuture next() { + System.out.printf("Delegate called!"); + System.out.flush(); + return null; + } + }; + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(context.getActivity().getRelatesTo()); + + try { + context.sendActivity(typingActivity).join(); + System.out.printf("HI"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(); + } + } + + @Test + public final void Transcript_LogActivities() { + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TestAdapter adapter = (new TestAdapter()).use( + new TranscriptLoggerMiddleware(transcriptStore) + ); + final String[] conversationId = { null }; + + new TestFlow(adapter, (context) -> { + delay(500); + conversationId[0] = context.getActivity().getConversation().getId(); + Activity typingActivity = new Activity(ActivityTypes.TYPING); + typingActivity.setRelatesTo(context.getActivity().getRelatesTo()); + + context.sendActivity(typingActivity).join(); + + delay(500); + + context.sendActivity("echo:" + context.getActivity().getText()).join(); + return CompletableFuture.completedFuture(null); + } + ).send("foo").delay(50).assertReply( + (activity) -> Assert.assertEquals(activity.getType(), ActivityTypes.TYPING) + ).assertReply("echo:foo").send("bar").delay(50).assertReply( + (activity) -> Assert.assertEquals(activity.getType(), ActivityTypes.TYPING) + ).assertReply("echo:bar").startTest().join(); + + PagedResult pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0] + ).join(); + Assert.assertEquals(6, pagedResult.getItems().size()); + Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText()); + Assert.assertNotEquals(pagedResult.getItems().get(1), null); + Assert.assertEquals("echo:foo", ((Activity) pagedResult.getItems().get(2)).getText()); + Assert.assertEquals("bar", ((Activity) pagedResult.getItems().get(3)).getText()); + + Assert.assertTrue(pagedResult.getItems().get(4) != null); + Assert.assertEquals("echo:bar", ((Activity) pagedResult.getItems().get(5)).getText()); + for (Object activity : pagedResult.getItems()) { + Assert.assertFalse(StringUtils.isBlank(((Activity) activity).getId())); + Assert.assertTrue(((Activity) activity).getTimestamp().isAfter(OffsetDateTime.MIN)); + } + System.out.printf("Complete"); + } + + @Test + public void Transcript_LogUpdateActivities() { + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TestAdapter adapter = (new TestAdapter()).use( + new TranscriptLoggerMiddleware(transcriptStore) + ); + final String[] conversationId = { null }; + final Activity[] activityToUpdate = { null }; + new TestFlow(adapter, (context) -> { + delay(500); + conversationId[0] = context.getActivity().getConversation().getId(); + if (context.getActivity().getText().equals("update")) { + activityToUpdate[0].setText("new response"); + context.updateActivity(activityToUpdate[0]).join(); + } else { + Activity activity = context.getActivity().createReply("response"); + ResourceResponse response = context.sendActivity(activity).join(); + activity.setId(response.getId()); + + // clone the activity, so we can use it to do an update + activityToUpdate[0] = Activity.clone(activity); + } + + return CompletableFuture.completedFuture(null); + } + ).send("foo").delay(50).send("update").delay(50).assertReply( + "new response" + ).startTest().join(); + + PagedResult pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0] + ).join(); + Assert.assertEquals(4, pagedResult.getItems().size()); + Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText()); + Assert.assertEquals("response", ((Activity) pagedResult.getItems().get(1)).getText()); + Assert.assertEquals("new response", ((Activity) pagedResult.getItems().get(2)).getText()); + Assert.assertEquals("update", ((Activity) pagedResult.getItems().get(3)).getText()); + Assert.assertEquals( + ((Activity) pagedResult.getItems().get(1)).getId(), + ((Activity) pagedResult.getItems().get(2)).getId() + ); + } + + @Test + public final void Transcript_LogDeleteActivities() { + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TestAdapter adapter = (new TestAdapter()).use( + new TranscriptLoggerMiddleware(transcriptStore) + ); + final String[] conversationId = { null }; + final String[] activityId = { null }; + new TestFlow(adapter, (context) -> { + delay(500); + conversationId[0] = context.getActivity().getConversation().getId(); + if (context.getActivity().getText().equals("deleteIt")) { + context.deleteActivity(activityId[0]).join(); + } else { + Activity activity = context.getActivity().createReply("response"); + ResourceResponse response = context.sendActivity(activity).join(); + activityId[0] = response.getId(); + } + + return CompletableFuture.completedFuture(null); + }).send("foo").delay(50).assertReply("response").send("deleteIt").startTest().join(); + + PagedResult pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0] + ).join(); + for (Object act : pagedResult.getItems()) { + System.out.printf( + "Here is the object: %s : Type: %s\n", + act.getClass().getTypeName(), + ((Activity) act).getType() + ); + } + + for (Object activity : pagedResult.getItems()) { + System.out.printf( + "Recipient: %s\nText: %s\n", + ((Activity) activity).getRecipient().getName(), + ((Activity) activity).getText() + ); + } + Assert.assertEquals(4, pagedResult.getItems().size()); + Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText()); + Assert.assertEquals("response", ((Activity) pagedResult.getItems().get(1)).getText()); + Assert.assertEquals("deleteIt", ((Activity) pagedResult.getItems().get(2)).getText()); + Assert.assertEquals( + ActivityTypes.MESSAGE_DELETE, + ((Activity) pagedResult.getItems().get(3)).getType() + ); + Assert.assertEquals( + ((Activity) pagedResult.getItems().get(1)).getId(), + ((Activity) pagedResult.getItems().get(3)).getId() + ); + } + + @Test + public void Transcript_TestDateLogUpdateActivities() { + OffsetDateTime dateTimeStartOffset1 = OffsetDateTime.now(); + OffsetDateTime dateTimeStartOffset2 = OffsetDateTime.now(ZoneId.of("UTC")); + + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TestAdapter adapter = (new TestAdapter()).use( + new TranscriptLoggerMiddleware(transcriptStore) + ); + + final String[] conversationId = { null }; + final Activity[] activityToUpdate = { null }; + new TestFlow(adapter, (context) -> { + delay(500); + conversationId[0] = context.getActivity().getConversation().getId(); + if (context.getActivity().getText().equals("update")) { + activityToUpdate[0].setText("new response"); + context.updateActivity(activityToUpdate[0]).join(); + } else { + Activity activity = context.getActivity().createReply("response"); + ResourceResponse response = context.sendActivity(activity).join(); + activity.setId(response.getId()); + + activityToUpdate[0] = Activity.clone(activity); + } + + return CompletableFuture.completedFuture(null); + } + ).send("foo").delay(50).send("update").delay(50).assertReply( + "new response" + ).startTest().join(); + + PagedResult pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0], + null, + dateTimeStartOffset1 + ).join(); + Assert.assertEquals(4, pagedResult.getItems().size()); + Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText()); + Assert.assertEquals("response", ((Activity) pagedResult.getItems().get(1)).getText()); + Assert.assertEquals("new response", ((Activity) pagedResult.getItems().get(2)).getText()); + Assert.assertEquals("update", ((Activity) pagedResult.getItems().get(3)).getText()); + Assert.assertEquals( + ((Activity) pagedResult.getItems().get(1)).getId(), + ((Activity) pagedResult.getItems().get(2)).getId() + ); + + pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0], + null, + OffsetDateTime.MIN + ).join(); + Assert.assertEquals(4, pagedResult.getItems().size()); + Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText()); + Assert.assertEquals("response", ((Activity) pagedResult.getItems().get(1)).getText()); + Assert.assertEquals("new response", ((Activity) pagedResult.getItems().get(2)).getText()); + Assert.assertEquals("update", ((Activity) pagedResult.getItems().get(3)).getText()); + Assert.assertEquals( + ((Activity) pagedResult.getItems().get(1)).getId(), + ((Activity) pagedResult.getItems().get(2)).getId() + ); + + pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0], + null, + OffsetDateTime.MAX + ).join(); + Assert.assertEquals(0, pagedResult.getItems().size()); + } + + @Test + public final void Transcript_RolesAreFilled() { + MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore(); + TestAdapter adapter = (new TestAdapter()).use( + new TranscriptLoggerMiddleware(transcriptStore) + ); + final String[] conversationId = { null }; + + new TestFlow(adapter, (context) -> { + delay(500); + // The next assert implicitly tests the immutability of the incoming + // message. As demonstrated by the asserts after this TestFlow block + // the role attribute is present on the activity as it is passed to + // the transcript, but still missing inside the flow + Assert.assertNotNull(context.getActivity().getFrom().getRole()); + conversationId[0] = context.getActivity().getConversation().getId(); + context.sendActivity("echo:" + context.getActivity().getText()).join(); + return CompletableFuture.completedFuture(null); + }).send("test").startTest().join(); + + PagedResult pagedResult = transcriptStore.getTranscriptActivities( + "test", + conversationId[0] + ).join(); + Assert.assertEquals(2, pagedResult.getItems().size()); + Assert.assertNotNull(pagedResult.getItems().get(0).getFrom()); + Assert.assertEquals(RoleTypes.USER, pagedResult.getItems().get(0).getFrom().getRole()); + Assert.assertNotNull(pagedResult.getItems().get(1).getFrom()); + Assert.assertEquals(RoleTypes.BOT, pagedResult.getItems().get(1).getFrom().getRole()); + + System.out.printf("Complete"); + } + + /** + * Time period delay. + * @param milliseconds Time to delay. + */ + private void delay(int milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java new file mode 100644 index 000000000..b16fabf11 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/TurnContextTests.java @@ -0,0 +1,645 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.adapters.TestFlow; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Attachments; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.restclient.RestClient; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class TurnContextTests { + @Test(expected = IllegalArgumentException.class) + public void ConstructorNullAdapter() { + new TurnContextImpl(null, new Activity(ActivityTypes.MESSAGE)); + Assert.fail("Should Fail due to null Adapter"); + } + + @Test(expected = IllegalArgumentException.class) + public void ConstructorNullActivity() { + new TurnContextImpl(new TestAdapter(), null); + Assert.fail("Should Fail due to null Activity"); + } + + @Test + public void Constructor() { + new TurnContextImpl(new TestAdapter(), new Activity(ActivityTypes.MESSAGE)); + } + + @Test + public void CacheValueUsingSetAndGet() { + TestAdapter adapter = new TestAdapter(); + new TestFlow(adapter, (turnContext -> { + switch (turnContext.getActivity().getText()) { + case "count": + return turnContext.sendActivity( + turnContext.getActivity().createReply("one") + ).thenCompose( + resourceResponse -> turnContext.sendActivity( + turnContext.getActivity().createReply("two") + ) + ).thenCompose( + resourceResponse -> turnContext.sendActivity( + turnContext.getActivity().createReply("two") + ) + ).thenApply(resourceResponse -> null); + + case "ignore": + break; + + case "TestResponded": + if (turnContext.getResponded()) { + return Async.completeExceptionally(new RuntimeException("Responded is true")); + } + + return turnContext.sendActivity( + turnContext.getActivity().createReply("one") + ).thenApply(resourceResponse -> { + if (!turnContext.getResponded()) { + throw new RuntimeException("Responded is false"); + } + return null; + }); + + default: + return turnContext.sendActivity( + turnContext.getActivity().createReply( + "echo:" + turnContext.getActivity().getText() + ) + ).thenApply(resourceResponse -> null); + } + + return CompletableFuture.completedFuture(null); + })).send("TestResponded").startTest().join(); + } + + @Test(expected = IllegalArgumentException.class) + public void GetThrowsOnNullKey() { + TurnContext c = new TurnContextImpl( + new SimpleAdapter(), + new Activity(ActivityTypes.MESSAGE) + ); + Object o = c.getTurnState().get((String) null); + } + + @Test + public void GetReturnsNullOnEmptyKey() { + TurnContext c = new TurnContextImpl( + new SimpleAdapter(), + new Activity(ActivityTypes.MESSAGE) + ); + Object service = c.getTurnState().get(""); + Assert.assertNull("Should not have found a service under an empty key", service); + } + + @Test + public void GetReturnsNullWithUnknownKey() { + TurnContext c = new TurnContextImpl( + new SimpleAdapter(), + new Activity(ActivityTypes.MESSAGE) + ); + Object service = c.getTurnState().get("test"); + Assert.assertNull("Should not have found a service with unknown key", service); + } + + @Test + public void CacheValueUsingGetAndSet() { + TurnContext c = new TurnContextImpl( + new SimpleAdapter(), + new Activity(ActivityTypes.MESSAGE) + ); + + c.getTurnState().add("bar", "foo"); + String result = c.getTurnState().get("bar"); + + Assert.assertEquals("foo", result); + } + + @Test + public void CacheValueUsingGetAndSetGenericWithTypeAsKeyName() { + TurnContext c = new TurnContextImpl( + new SimpleAdapter(), + new Activity(ActivityTypes.MESSAGE) + ); + + c.getTurnState().add("foo"); + String result = c.getTurnState().get(String.class); + + Assert.assertEquals("foo", result); + } + + @Test + public void RequestIsSet() { + TurnContext c = new TurnContextImpl(new SimpleAdapter(), TestMessage.Message()); + Assert.assertEquals("1234", c.getActivity().getId()); + } + + @Test + public void SendAndSetResponded() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); + ResourceResponse response = c.sendActivity(TestMessage.Message("testtest")).join(); + + Assert.assertTrue(c.getResponded()); + Assert.assertEquals("testtest", response.getId()); + } + + @Test + public void SendBatchOfActivities() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); + + Activity message1 = TestMessage.Message("message1"); + Activity message2 = TestMessage.Message("message2"); + + ResourceResponse[] response = c.sendActivities(Arrays.asList(message1, message2)).join(); + + Assert.assertTrue(c.getResponded()); + Assert.assertEquals(2, response.length); + Assert.assertEquals("message1", response[0].getId()); + Assert.assertEquals("message2", response[1].getId()); + } + + @Test + public void SendAndSetRespondedUsingIMessageActivity() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); + + Activity msg = TestMessage.Message(); + c.sendActivity(msg).join(); + Assert.assertTrue(c.getResponded()); + } + + @Test + public void TraceActivitiesDoNoSetResponded() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + Assert.assertFalse(c.getResponded()); + + // Send a Trace Activity, and make sure responded is NOT set. + Activity trace = Activity.createTraceActivity("trace"); + c.sendActivity(trace).join(); + Assert.assertFalse(c.getResponded()); + + // Just to sanity check everything, send a Message and verify the + // responded flag IS set. + Activity msg = TestMessage.Message(); + c.sendActivity(msg).join(); + Assert.assertTrue(c.getResponded()); + } + + @Test + public void SendOneActivityToAdapter() { + boolean[] foundActivity = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter((activities) -> { + Assert.assertTrue("Incorrect Count", activities.size() == 1); + Assert.assertEquals("1234", activities.get(0).getId()); + foundActivity[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + c.sendActivity(TestMessage.Message()).join(); + Assert.assertTrue(foundActivity[0]); + } + + @Test + public void CallOnSendBeforeDelivery() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + int[] count = new int[] { 0 }; + c.onSendActivities(((context, activities, next) -> { + Assert.assertNotNull(activities); + count[0] = activities.size(); + return next.get(); + })); + + c.sendActivity(TestMessage.Message()).join(); + + Assert.assertEquals(1, count[0]); + } + + @Test + public void AllowInterceptionOfDeliveryOnSend() { + boolean[] responsesSent = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter((activities) -> { + responsesSent[0] = true; + Assert.fail("Should not be called. Interceptor did not work"); + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + int[] count = new int[] { 0 }; + c.onSendActivities(((context, activities, next) -> { + Assert.assertNotNull(activities); + count[0] = activities.size(); + + // Do not call next. + return CompletableFuture.completedFuture(null); + })); + + c.sendActivity(TestMessage.Message()).join(); + Assert.assertEquals(1, count[0]); + Assert.assertFalse("Responses made it to the adapter.", responsesSent[0]); + } + + @Test + public void InterceptAndMutateOnSend() { + boolean[] foundIt = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter((activities) -> { + Assert.assertNotNull(activities); + Assert.assertTrue(activities.size() == 1); + Assert.assertEquals("changed", activities.get(0).getId()); + foundIt[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.onSendActivities(((context, activities, next) -> { + Assert.assertNotNull(activities); + Assert.assertTrue(activities.size() == 1); + Assert.assertEquals("1234", activities.get(0).getId()); + activities.get(0).setId("changed"); + return next.get(); + })); + + c.sendActivity(TestMessage.Message()).join(); + Assert.assertTrue(foundIt[0]); + } + + @Test + public void UpdateOneActivityToAdapter() { + boolean[] foundActivity = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertNotNull(activity); + Assert.assertEquals("test", activity.getId()); + foundActivity[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + ResourceResponse updateResult = c.updateActivity(TestMessage.Message("test")).join(); + Assert.assertTrue(foundActivity[0]); + Assert.assertEquals("test", updateResult.getId()); + } + + @Test + public void UpdateActivityWithMessageFactory() { + final String ACTIVITY_ID = "activity ID"; + final String CONVERSATION_ID = "conversation ID"; + + boolean[] foundActivity = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertNotNull(activity); + Assert.assertEquals(ACTIVITY_ID, activity.getId()); + Assert.assertEquals(CONVERSATION_ID, activity.getConversation().getId()); + foundActivity[0] = true; + }); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setConversation(new ConversationAccount(CONVERSATION_ID)); + TurnContext c = new TurnContextImpl(a, activity); + + Activity message = MessageFactory.text("test text"); + message.setId(ACTIVITY_ID); + + ResourceResponse updateResult = c.updateActivity(message).join(); + + Assert.assertTrue(foundActivity[0]); + Assert.assertEquals(ACTIVITY_ID, updateResult.getId()); + } + + @Test + public void CallOnUpdateBeforeDelivery() { + boolean[] activityDelivered = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertNotNull(activity); + Assert.assertEquals("1234", activity.getId()); + activityDelivered[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + boolean[] wasCalled = new boolean[] { false }; + c.onUpdateActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + Assert.assertFalse(activityDelivered[0]); + wasCalled[0] = true; + return next.get(); + })); + + c.updateActivity(TestMessage.Message()).join(); + + Assert.assertTrue(wasCalled[0]); + Assert.assertTrue(activityDelivered[0]); + } + + @Test + public void InterceptOnUpdate() { + boolean[] activityDelivered = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + activityDelivered[0] = true; + Assert.fail("Should not be called."); + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + boolean[] wasCalled = new boolean[] { false }; + c.onUpdateActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + wasCalled[0] = true; + + // Do Not Call Next + return CompletableFuture.completedFuture(null); + })); + + c.updateActivity(TestMessage.Message()).join(); + + Assert.assertTrue(wasCalled[0]); + Assert.assertFalse(activityDelivered[0]); + } + + @Test + public void InterceptAndMutateOnUpdate() { + boolean[] activityDelivered = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, (activity) -> { + Assert.assertEquals("mutated", activity.getId()); + activityDelivered[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.onUpdateActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + Assert.assertEquals("1234", activity.getId()); + activity.setId("mutated"); + return next.get(); + })); + + c.updateActivity(TestMessage.Message()).join(); + + Assert.assertTrue(activityDelivered[0]); + } + + @Test + public void DeleteOneActivityToAdapter() { + boolean[] activityDeleted = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + Assert.assertEquals("12345", reference.getActivityId()); + activityDeleted[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, TestMessage.Message()); + + c.deleteActivity("12345").join(); + Assert.assertTrue(activityDeleted[0]); + } + + @Test + public void DeleteConversationReferenceToAdapter() { + boolean[] activityDeleted = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + Assert.assertEquals("12345", reference.getActivityId()); + activityDeleted[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, TestMessage.Message()); + + ConversationReference reference = new ConversationReference(); + reference.setActivityId("12345"); + + c.deleteActivity(reference).join(); + Assert.assertTrue(activityDeleted[0]); + } + + @Test + public void InterceptOnDelete() { + boolean[] activityDeleted = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + activityDeleted[0] = true; + Assert.fail("Should not be called."); + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + boolean[] wasCalled = new boolean[] { false }; + c.onDeleteActivity(((context, activity, next) -> { + Assert.assertNotNull(activity); + wasCalled[0] = true; + + // Do Not Call Next + return CompletableFuture.completedFuture(null); + })); + + c.deleteActivity("1234").join(); + + Assert.assertTrue(wasCalled[0]); + Assert.assertFalse(activityDeleted[0]); + } + + @Test + public void DeleteWithNoOnDeleteHandlers() { + boolean[] activityDeleted = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, null, (activity) -> { + activityDeleted[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.deleteActivity("1234").join(); + + Assert.assertTrue(activityDeleted[0]); + } + + @Test + public void InterceptAndMutateOnDelete() { + boolean[] activityDeleted = new boolean[] { false }; + + SimpleAdapter a = new SimpleAdapter(null, null, (reference) -> { + Assert.assertEquals("mutated", reference.getActivityId()); + activityDeleted[0] = true; + }); + + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.onDeleteActivity(((context, reference, next) -> { + Assert.assertNotNull(reference); + Assert.assertEquals("1234", reference.getActivityId()); + reference.setActivityId("mutated"); + return next.get(); + })); + + c.deleteActivity("1234").join(); + + Assert.assertTrue(activityDeleted[0]); + } + + @Test + public void ThrowExceptionInOnSend() { + SimpleAdapter a = new SimpleAdapter(); + TurnContext c = new TurnContextImpl(a, new Activity(ActivityTypes.MESSAGE)); + + c.onSendActivities(((context, activities, next) -> { + CompletableFuture result = new CompletableFuture(); + result.completeExceptionally(new RuntimeException("test")); + return result; + })); + + try { + c.sendActivity(TestMessage.Message()).join(); + Assert.fail("ThrowExceptionInOnSend have thrown"); + } catch (CompletionException e) { + Assert.assertEquals("test", e.getCause().getMessage()); + } + } + + @Test + public void TurnContextStateNoDispose() { + ConnectorClient connector = new ConnectorClientThrowExceptionOnDispose(); + Assert.assertTrue(connector instanceof AutoCloseable); + + TurnContextStateCollection stateCollection = new TurnContextStateCollection(); + stateCollection.add("connector", connector); + + try { + stateCollection.close(); + } catch (Throwable t) { + Assert.fail("Should not have thrown"); + } + } + + @Test + public void TurnContextStateDisposeNonConnectorClient() { + TrackDisposed disposableObject1 = new TrackDisposed(); + TrackDisposed disposableObject2 = new TrackDisposed(); + TrackDisposed disposableObject3 = new TrackDisposed(); + Assert.assertFalse(disposableObject1.disposed); + Assert.assertFalse(disposableObject2.disposed); + Assert.assertFalse(disposableObject3.disposed); + + ConnectorClient connector = new ConnectorClientThrowExceptionOnDispose(); + + TurnContextStateCollection stateCollection = new TurnContextStateCollection(); + stateCollection.add("disposable1", disposableObject1); + stateCollection.add("disposable2", disposableObject2); + stateCollection.add("disposable3", disposableObject3); + stateCollection.add("connector", connector); + + try { + stateCollection.close(); + } catch (Throwable t) { + Assert.fail("Should not have thrown"); + } + + Assert.assertTrue(disposableObject1.disposed); + Assert.assertTrue(disposableObject2.disposed); + Assert.assertTrue(disposableObject3.disposed); + } + + private static class TrackDisposed implements AutoCloseable { + public boolean disposed = false; + + @Override + public void close() throws Exception { + disposed = true; + } + } + + private static class ConnectorClientThrowExceptionOnDispose implements ConnectorClient { + + @Override + public RestClient getRestClient() { + return null; + } + + @Override + public String getUserAgent() { + return null; + } + + @Override + public String getAcceptLanguage() { + return null; + } + + @Override + public void setAcceptLanguage(String acceptLanguage) { + + } + + @Override + public int getLongRunningOperationRetryTimeout() { + return 0; + } + + @Override + public void setLongRunningOperationRetryTimeout(int timeout) { + + } + + @Override + public boolean getGenerateClientRequestId() { + return false; + } + + @Override + public void setGenerateClientRequestId(boolean generateClientRequestId) { + + } + + @Override + public String baseUrl() { + return null; + } + + @Override + public ServiceClientCredentials credentials() { + return null; + } + + @Override + public Attachments getAttachments() { + return null; + } + + @Override + public Conversations getConversations() { + return null; + } + + @Override + public void close() throws Exception { + throw new RuntimeException("Should not close"); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java new file mode 100644 index 000000000..ce265c2e3 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java @@ -0,0 +1,690 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.adapters; + +import com.microsoft.bot.builder.*; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.schema.*; +import org.apache.commons.lang3.StringUtils; +import org.junit.rules.ExpectedException; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class TestAdapter extends BotAdapter implements UserTokenProvider { + + private final String exceptionExpected = "ExceptionExpected"; + private final Queue botReplies = new LinkedList<>(); + private int nextId = 0; + private ConversationReference conversationReference; + private String locale = "en-us"; + private boolean sendTraceActivity = false; + private Map exchangableToken = new HashMap(); + + + private static class UserTokenKey { + private String connectionName; + private String userId; + private String channelId; + + public String getConnectionName() { + return connectionName; + } + + public void setConnectionName(String withConnectionName) { + connectionName = withConnectionName; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String withUserId) { + userId = withUserId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String withChannelId) { + channelId = withChannelId; + } + + + + @Override + public boolean equals(Object rhs) { + if (!(rhs instanceof UserTokenKey)) + return false; + return StringUtils.equals(connectionName, ((UserTokenKey) rhs).connectionName) + && StringUtils.equals(userId, ((UserTokenKey) rhs).userId) + && StringUtils.equals(channelId, ((UserTokenKey) rhs).channelId); + } + + @Override + public int hashCode() { + return Objects.hash(connectionName, userId, channelId); + } + } + + private static class TokenMagicCode { + public UserTokenKey key; + public String magicCode; + public String userToken; + } + + private Map userTokens = new HashMap<>(); + private List magicCodes = new ArrayList<>(); + + public TestAdapter() { + this((ConversationReference) null); + } + + public TestAdapter(String channelId) { + this(channelId, false); + } + + public TestAdapter(String channelId, boolean sendTraceActivity) { + this.sendTraceActivity = sendTraceActivity; + + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setChannelId(channelId); + conversationReference.setServiceUrl("https://test.com"); + ChannelAccount userAccount = new ChannelAccount(); + userAccount.setId("user1"); + userAccount.setName("User1"); + conversationReference.setUser(userAccount); + ChannelAccount botAccount = new ChannelAccount(); + botAccount.setId("bot"); + botAccount.setName("Bot"); + conversationReference.setBot(botAccount); + ConversationAccount conversation = new ConversationAccount(); + conversation.setIsGroup(false); + conversation.setConversationType("convo1"); + conversation.setId("Conversation1"); + conversationReference.setConversation(conversation); + conversationReference.setLocale(this.getLocale()); + + setConversationReference(conversationReference); + } + + public TestAdapter(ConversationReference reference) { + if (reference != null) { + setConversationReference(reference); + } else { + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setChannelId(Channels.TEST); + conversationReference.setServiceUrl("https://test.com"); + ChannelAccount userAccount = new ChannelAccount(); + userAccount.setId("user1"); + userAccount.setName("User1"); + conversationReference.setUser(userAccount); + ChannelAccount botAccount = new ChannelAccount(); + botAccount.setId("bot"); + botAccount.setName("Bot"); + conversationReference.setBot(botAccount); + ConversationAccount conversation = new ConversationAccount(); + conversation.setIsGroup(false); + conversation.setConversationType("convo1"); + conversation.setId("Conversation1"); + conversationReference.setConversation(conversation); + conversationReference.setLocale(this.getLocale()); + setConversationReference(conversationReference); + } + } + public TestAdapter(ConversationReference reference, boolean sendTraceActivity) { + this(reference); + this.sendTraceActivity = sendTraceActivity; + } + + public Queue activeQueue() { + return botReplies; + } + + @Override + public TestAdapter use(Middleware middleware) { + super.use(middleware); + return this; + } + + /** + * Adds middleware to the adapter to register an Storage object on the turn + * context. The middleware registers the state objects on the turn context at + * the start of each turn. + * + * @param storage The storage object to register. + * @return The updated adapter. + */ + public TestAdapter useStorage(Storage storage) { + if (storage == null) { + throw new IllegalArgumentException("Storage cannot be null"); + } + return this.use(new RegisterClassMiddleware(storage)); + } + + /** + * Adds middleware to the adapter to register one or more BotState objects on + * the turn context. The middleware registers the state objects on the turn + * context at the start of each turn. + * + * @param botstates The state objects to register. + * @return The updated adapter. + */ + public TestAdapter useBotState(BotState... botstates) { + if (botstates == null) { + throw new IllegalArgumentException("botstates cannot be null"); + } + for (BotState botState : botstates) { + this.use(new RegisterClassMiddleware(botState)); + } + return this; + } + + public CompletableFuture processActivity(Activity activity, BotCallbackHandler callback) { + return CompletableFuture.supplyAsync(() -> { + synchronized (conversationReference()) { + // ready for next reply + if (activity.getType() == null) + activity.setType(ActivityTypes.MESSAGE); + + if (activity.getChannelId() == null) { + activity.setChannelId(conversationReference().getChannelId()); + } + + if (activity.getFrom() == null || StringUtils.equalsIgnoreCase(activity.getFrom().getId(), "unknown") + || activity.getFrom().getRole() == RoleTypes.BOT) { + activity.setFrom(conversationReference().getUser()); + } + + activity.setRecipient(conversationReference().getBot()); + activity.setConversation(conversationReference().getConversation()); + activity.setServiceUrl(conversationReference().getServiceUrl()); + + Integer next = nextId++; + activity.setId(next.toString()); + } + // Assume Default DateTime : DateTime(0) + if (activity.getTimestamp() == null || activity.getTimestamp().toEpochSecond() == 0) + activity.setTimestamp(OffsetDateTime.now(ZoneId.of("UTC"))); + + if (activity.getLocalTimestamp() == null || activity.getLocalTimestamp().toEpochSecond() == 0) + activity.setLocalTimestamp(OffsetDateTime.now()); + + return activity; + }).thenCompose(activity1 -> { + TurnContextImpl context = new TurnContextImpl(this, activity1); + return super.runPipeline(context, callback); + }); + } + + public ConversationReference conversationReference() { + return conversationReference; + } + + public void setConversationReference(ConversationReference conversationReference) { + this.conversationReference = conversationReference; + } + + @Override + public CompletableFuture sendActivities(TurnContext context, List activities) { + List responses = new LinkedList(); + + for (Activity activity : activities) { + if (StringUtils.isEmpty(activity.getId())) + activity.setId(UUID.randomUUID().toString()); + + if (activity.getTimestamp() == null) + activity.setTimestamp(OffsetDateTime.now(ZoneId.of("UTC"))); + + responses.add(new ResourceResponse(activity.getId())); + + System.out.println(String.format("TestAdapter:SendActivities, Count:%s (tid:%s)", activities.size(), + Thread.currentThread().getId())); + for (Activity act : activities) { + System.out.printf(" :--------\n : To:%s\n", (act.getRecipient() == null) ? "No recipient set" : act.getRecipient().getName()); + System.out.printf(" : From:%s\n", (act.getFrom() == null) ? "No from set" : act.getFrom().getName()); + System.out.printf(" : Text:%s\n :---------\n", (act.getText() == null) ? "No text set" : act.getText()); + } + + // This is simulating DELAY + if (activity.getType().toString().equals("delay")) { + // The BotFrameworkAdapter and Console adapter implement this + // hack directly in the POST method. Replicating that here + // to keep the behavior as close as possible to facilitate + // more realistic tests. + int delayMs = (int) activity.getValue(); + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + } + } else if (activity.getType().equals(ActivityTypes.TRACE)) { + if (sendTraceActivity) { + synchronized (botReplies) { + botReplies.add(activity); + } + } + } else { + synchronized (botReplies) { + botReplies.add(activity); + } + } + } + return CompletableFuture.completedFuture(responses.toArray(new ResourceResponse[responses.size()])); + } + + @Override + public CompletableFuture updateActivity(TurnContext context, Activity activity) { + synchronized (botReplies) { + List replies = new ArrayList<>(botReplies); + for (int i = 0; i < botReplies.size(); i++) { + if (replies.get(i).getId().equals(activity.getId())) { + replies.set(i, activity); + botReplies.clear(); + + for (Activity item : replies) { + botReplies.add(item); + } + return CompletableFuture.completedFuture(new ResourceResponse(activity.getId())); + } + } + } + return CompletableFuture.completedFuture(new ResourceResponse()); + } + + @Override + public CompletableFuture deleteActivity(TurnContext context, ConversationReference reference) { + synchronized (botReplies) { + ArrayList replies = new ArrayList<>(botReplies); + for (int i = 0; i < botReplies.size(); i++) { + if (replies.get(i).getId().equals(reference.getActivityId())) { + replies.remove(i); + botReplies.clear(); + for (Activity item : replies) { + botReplies.add(item); + } + break; + } + } + } + return CompletableFuture.completedFuture(null); + } + + /** + * Called by TestFlow to check next reply + * + * @return + */ + public Activity getNextReply() { + synchronized (botReplies) { + if (botReplies.size() > 0) { + return botReplies.remove(); + } + } + return null; + } + + /** + * Called by TestFlow to get appropriate activity for conversationReference of + * testbot + * + * @return + */ + public Activity makeActivity() { + return makeActivity(null); + } + + public Activity makeActivity(String withText) { + Integer next = nextId++; + String locale = !getLocale().isEmpty() ? getLocale() : "en-us"; + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setLocale(locale); + activity.setFrom(conversationReference().getUser()); + activity.setRecipient(conversationReference().getBot()); + activity.setConversation(conversationReference().getConversation()); + activity.setServiceUrl(conversationReference().getServiceUrl()); + activity.setId(next.toString()); + activity.setText(withText); + activity.setLocale(getLocale() != null ? getLocale() : "en-us"); + + return activity; + } + + /** + * Called by TestFlow to send text to the bot + * + * @param userSays + * @return + */ + public CompletableFuture sendTextToBot(String userSays, BotCallbackHandler callback) { + return processActivity(this.makeActivity(userSays), callback); + } + + public void addUserToken(String connectionName, String channelId, String userId, String token, + String withMagicCode) { + UserTokenKey userKey = new UserTokenKey(); + userKey.connectionName = connectionName; + userKey.channelId = channelId; + userKey.userId = userId; + + if (withMagicCode == null) { + userTokens.put(userKey, token); + } else { + TokenMagicCode tokenMagicCode = new TokenMagicCode(); + tokenMagicCode.key = userKey; + tokenMagicCode.magicCode = withMagicCode; + tokenMagicCode.userToken = token; + magicCodes.add(tokenMagicCode); + } + } + + public CompletableFuture getUserToken(TurnContext turnContext, String connectionName, + String magicCode) { + return getUserToken(turnContext, null, connectionName, magicCode); + + } + public CompletableFuture signOutUser(TurnContext turnContext, String connectionName, String userId) { + return signOutUser(turnContext, null, connectionName, userId); + } + + public CompletableFuture> getTokenStatus(TurnContext turnContext, String userId, + String includeFilter) { + return getTokenStatus(turnContext, null, userId, includeFilter); + } + + public CompletableFuture> getAadTokens(TurnContext turnContext, String connectionName, + String[] resourceUrls, String userId) { + return getAadTokens(turnContext, null, connectionName, resourceUrls, userId); + } + + public static ConversationReference createConversationReference(String name, String user, String bot) { + ConversationReference reference = new ConversationReference(); + reference.setChannelId("test"); + reference.setServiceUrl("https://test.com"); + reference.setConversation(new ConversationAccount(false, name, name, null, null, null, null)); + reference.setUser(new ChannelAccount(user.toLowerCase(), user)); + reference.setBot(new ChannelAccount(bot.toLowerCase(), bot)); + reference.setLocale("en-us"); + return reference; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + public String getLocale() { + return locale; + } + + public void setSendTraceActivity(boolean sendTraceActivity) { + this.sendTraceActivity = sendTraceActivity; + } + + public boolean getSendTraceActivity() { + return sendTraceActivity; + } + + public CompletableFuture getOAuthSignInLink(TurnContext turnContext, String connectionName) { + return getOAuthSignInLink(turnContext, null, connectionName); + } + + public CompletableFuture getOAuthSignInLink(TurnContext turnContext, String connectionName, String userId, + String finalRedirect) { + return getOAuthSignInLink(turnContext, null, connectionName, userId, finalRedirect); + } + + @Override + public CompletableFuture getOAuthSignInLink(TurnContext turnContext, AppCredentials oAuthAppCredentials, + String connectionName) { + return CompletableFuture.completedFuture( + String.format("https://fake.com/oauthsignin/%s/%s", + connectionName, + turnContext.getActivity().getChannelId())); + } + + public CompletableFuture getOAuthSignInLink(TurnContext turnContext, AppCredentials oAuthAppCredentials, + String connectionName, String userId, String finalRedirect) { + return CompletableFuture.completedFuture( + String.format("https://fake.com/oauthsignin/%s/%s/%s", + connectionName, + turnContext.getActivity().getChannelId(), + userId)); + } + + @Override + public CompletableFuture getUserToken(TurnContext turnContext, AppCredentials oAuthAppCredentials, + String connectionName, String magicCode) { + UserTokenKey key = new UserTokenKey(); + key.connectionName = connectionName; + key.channelId = turnContext.getActivity().getChannelId(); + key.userId = turnContext.getActivity().getFrom().getId(); + + if (magicCode != null) { + TokenMagicCode magicCodeRecord = magicCodes.stream() + .filter(tokenMagicCode -> key.equals(tokenMagicCode.key)).findFirst().orElse(null); + if (magicCodeRecord != null && StringUtils.equals(magicCodeRecord.magicCode, magicCode)) { + addUserToken(connectionName, key.channelId, key.userId, magicCodeRecord.userToken, null); + } + } + + if (userTokens.containsKey(key)) { + TokenResponse tokenResponse = new TokenResponse(); + tokenResponse.setConnectionName(connectionName); + tokenResponse.setToken(userTokens.get(key)); + return CompletableFuture.completedFuture(tokenResponse); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Adds a fake exchangeable token so it can be exchanged later. + * @param connectionName he connection name. + * @param channelId The channel ID. + * @param userId The user ID. + * @param exchangableItem The exchangeable token or resource URI. + * @param token The token to store. + */ + public void addExchangeableToken(String connectionName, + String channelId, + String userId, + String exchangableItem, + String token + ) { + ExchangableTokenKey key = new ExchangableTokenKey(); + key.setConnectionName(connectionName); + key.setChannelId(channelId); + key.setUserId(userId); + key.setExchangableItem(exchangableItem); + + if (exchangableToken.containsKey(key)) { + exchangableToken.replace(key, token); + } else { + exchangableToken.put(key, token); + } + } + + public void throwOnExchangeRequest(String connectionName, + String channelId, + String userId, + String exchangableItem) { + ExchangableTokenKey key = new ExchangableTokenKey(); + key.setConnectionName(connectionName); + key.setChannelId(channelId); + key.setUserId(userId); + key.setExchangableItem(exchangableItem); + + if (exchangableToken.containsKey(key)) { + exchangableToken.replace(key, exceptionExpected); + } else { + exchangableToken.put(key, exceptionExpected); + } + } + + @Override + public CompletableFuture signOutUser(TurnContext turnContext, AppCredentials oAuthAppCredentials, + String connectionName, String userId) { + String channelId = turnContext.getActivity().getChannelId(); + final String effectiveUserId = userId == null ? turnContext.getActivity().getFrom().getId() : userId; + + userTokens.keySet().stream() + .filter(t -> StringUtils.equals(t.channelId, channelId) + && StringUtils.equals(t.userId, effectiveUserId) + && connectionName == null || StringUtils.equals(t.connectionName, connectionName)) + .collect(Collectors.toList()).forEach(key -> userTokens.remove(key)); + + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> getTokenStatus(TurnContext turnContext, + AppCredentials oAuthAppCredentials, + String userId, + String includeFilter) { + String[] filter = includeFilter == null ? null : includeFilter.split(","); + List records = userTokens.keySet().stream() + .filter(x -> StringUtils.equals(x.channelId, turnContext.getActivity().getChannelId()) + && StringUtils.equals(x.userId, turnContext.getActivity().getFrom().getId()) + && (includeFilter == null || Arrays.binarySearch(filter, x.connectionName) != -1)) + .map(r -> { + TokenStatus tokenStatus = new TokenStatus(); + tokenStatus.setConnectionName(r.connectionName); + tokenStatus.setHasToken(true); + tokenStatus.setServiceProviderDisplayName(r.connectionName); + return tokenStatus; + }).collect(Collectors.toList()); + + if (records.size() > 0) { + return CompletableFuture.completedFuture(records); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> getAadTokens(TurnContext context, + AppCredentials oAuthAppCredentials, String connectionName, String[] resourceUrls, String userId) { + return CompletableFuture.completedFuture(new HashMap<>()); + } + + @Override + public CompletableFuture getSignInResource(TurnContext turnContext, String connectionName) { + String id = null; + if (turnContext != null + && turnContext.getActivity() != null + && turnContext.getActivity().getRecipient() != null + && turnContext.getActivity().getRecipient().getId() != null) { + id = turnContext.getActivity().getRecipient().getId(); + } + + return getSignInResource(turnContext, connectionName, id, null); + } + + @Override + public CompletableFuture getSignInResource(TurnContext turnContext, String connectionName, + String userId, String finalRedirect) { + return getSignInResource(turnContext, null, connectionName, userId, finalRedirect); + } + + @Override + public CompletableFuture getSignInResource(TurnContext turnContext, + AppCredentials oAuthAppCredentials, String connectionName, String userId, String finalRedirect) { + + SignInResource signInResource = new SignInResource(); + signInResource.setSignInLink( + String.format("https://fake.com/oauthsignin/%s/%s/%s", + connectionName, + turnContext.getActivity().getChannelId(), + userId)); + TokenExchangeResource tokenExchangeResource = new TokenExchangeResource(); + tokenExchangeResource.setId(UUID.randomUUID().toString()); + tokenExchangeResource.setProviderId(null); + tokenExchangeResource.setUri(String.format("api://%s/resource", connectionName)); + signInResource.setTokenExchangeResource(tokenExchangeResource); + return CompletableFuture.completedFuture(signInResource); + } + + @Override + public CompletableFuture exchangeToken(TurnContext turnContext, String connectionName, String userId, + TokenExchangeRequest exchangeRequest) { + return exchangeToken(turnContext, null, connectionName, userId, exchangeRequest); + } + + @Override + public CompletableFuture exchangeToken(TurnContext turnContext, AppCredentials oAuthAppCredentials, + String connectionName, String userId, TokenExchangeRequest exchangeRequest) { + + String exchangableValue = null; + if (exchangeRequest.getToken() != null) { + if (StringUtils.isNotBlank(exchangeRequest.getToken())) { + exchangableValue = exchangeRequest.getToken(); + } + } else { + if (exchangeRequest.getUri() != null) { + exchangableValue = exchangeRequest.getUri(); + } + } + + ExchangableTokenKey key = new ExchangableTokenKey(); + if (turnContext != null + && turnContext.getActivity() != null + && turnContext.getActivity().getChannelId() != null) { + key.setChannelId(turnContext.getActivity().getChannelId()); + } + key.setConnectionName(connectionName); + key.setExchangableItem(exchangableValue); + key.setUserId(userId); + + String token = exchangableToken.get(key); + if (token != null) { + if (token.equals(exceptionExpected)) { + return Async.completeExceptionally( + new RuntimeException("Exception occurred during exchanging tokens") + ); + } + TokenResponse tokenResponse = new TokenResponse(); + tokenResponse.setChannelId(key.getChannelId()); + tokenResponse.setConnectionName(key.getConnectionName()); + tokenResponse.setToken(token); + return CompletableFuture.completedFuture(tokenResponse); + } else { + return CompletableFuture.completedFuture(null); + } + + } + + class ExchangableTokenKey extends UserTokenKey { + + private String exchangableItem = ""; + + public String getExchangableItem() { + return exchangableItem; + } + + public void setExchangableItem(String withExchangableItem) { + exchangableItem = withExchangableItem; + } + + @Override + public boolean equals(Object rhs) { + if (!(rhs instanceof ExchangableTokenKey)) { + return false; + } + return StringUtils.equals(exchangableItem, ((ExchangableTokenKey) rhs).exchangableItem) + && super.equals(rhs); + } + + @Override + public int hashCode() { + return Objects.hash(exchangableItem != null ? exchangableItem : "") + super.hashCode(); + } + + + } + +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java new file mode 100644 index 000000000..0b817e8f7 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestFlow.java @@ -0,0 +1,518 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.adapters; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.BotCallbackHandler; +import com.microsoft.bot.connector.ExecutorFactory; +import com.microsoft.bot.schema.Activity; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; + +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class TestFlow { + final TestAdapter adapter; + CompletableFuture testTask; + BotCallbackHandler callback; + // ArrayList> tasks = new ArrayList>(); + + public TestFlow(TestAdapter adapter) { + this(adapter, null); + } + + public TestFlow( + TestAdapter adapter, + BotCallbackHandler callback + ) { + this.adapter = adapter; + this.callback = callback; + this.testTask = CompletableFuture.completedFuture(null); + } + + public TestFlow( + CompletableFuture testTask, + TestFlow flow + ) { + this.testTask = testTask == null ? CompletableFuture.completedFuture(null) : testTask; + this.callback = flow.callback; + this.adapter = flow.adapter; + } + + /** + * Start the execution of the test flow + * + * @return + */ + public CompletableFuture startTest() { + return testTask; + } + + /** + * Send a message from the user to the bot + * + * @param userSays + * @return + */ + public TestFlow send(String userSays) throws IllegalArgumentException { + if (userSays == null) + throw new IllegalArgumentException("You have to pass a userSays parameter"); + + return new TestFlow(testTask.thenCompose(result -> { + System.out.print( + String.format("USER SAYS: %s (tid: %s)\n", userSays, Thread.currentThread().getId()) + ); + return this.adapter.sendTextToBot(userSays, this.callback); + }), this); + } + + /** + * Creates a conversation update activity and process it the activity. + * @return A new TestFlow Object + */ + public TestFlow sendConversationUpdate() { + return new TestFlow(testTask.thenCompose(result -> { + Activity cu = Activity.createConversationUpdateActivity(); + cu.getMembersAdded().add(this.adapter.conversationReference().getUser()); + return this.adapter.processActivity(cu, callback); + }), this); + } + + /** + * Send an activity from the user to the bot. + * + * @param userActivity + * @return + */ + public TestFlow send(Activity userActivity) { + if (userActivity == null) + throw new IllegalArgumentException("You have to pass an Activity"); + + return new TestFlow(testTask.thenCompose(result -> { + System.out.printf( + "TestFlow: Send with User Activity! %s (tid:%s)", + userActivity.getText(), + Thread.currentThread().getId() + ); + return this.adapter.processActivity(userActivity, this.callback); + }), this); + } + + /** + * Delay for time period + * + * @param ms + * @return + */ + public TestFlow delay(int ms) { + return new TestFlow(testTask.thenCompose(result -> { + System.out.printf( + "TestFlow: Delay(%s ms) called. (tid:%s)\n", + ms, + Thread.currentThread().getId() + ); + System.out.flush(); + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + + } + return CompletableFuture.completedFuture(null); + }), this); + } + + /** + * Assert that reply is expected text + * + * @param expected + * @return + */ + public TestFlow assertReply(String expected) { + return this.assertReply(expected, null, 3000); + } + + public TestFlow assertReply(String expected, String description) { + return this.assertReply(expected, description, 3000); + } + + public TestFlow assertReply(String expected, String description, int timeout) { + return this.assertReply(this.adapter.makeActivity(expected), description, timeout); + } + + /** + * Assert that the reply is expected activity + * + * @param expected + * @return + */ + public TestFlow assertReply(Activity expected) { + String description = Thread.currentThread().getStackTrace()[1].getMethodName(); + return assertReply(expected, description); + } + + public TestFlow assertReply(Activity expected, String description) { + return assertReply(expected, description, 3000); + } + + public TestFlow assertReply(Activity expected, String description, int timeout) { + if (description == null) + description = Thread.currentThread().getStackTrace()[1].getMethodName(); + return this.assertReply((reply) -> { + if (!StringUtils.equals(expected.getType(), reply.getType())) + throw new RuntimeException( + String.format( + "Type: '%s' should match expected '%s'", + reply.getType(), + expected.getType() + ) + ); + if (!expected.getText().equals(reply.getText())) { + throw new RuntimeException( + String.format( + "Text '%s' should match expected '%s'", + reply.getText(), + expected.getText() + ) + ); + } + }, description, timeout); + } + + /** + * Assert that the reply matches a custom validation routine + * + * @param validateActivity + * @return + */ + public TestFlow assertReply(Consumer validateActivity) { + String description = Thread.currentThread().getStackTrace()[1].getMethodName(); + return assertReply(validateActivity, description, 3000); + } + + public TestFlow assertReply(Consumer validateActivity, String description) { + return assertReply(validateActivity, description, 3000); + } + + public TestFlow assertReply( + Consumer validateActivity, + String description, + int timeout + ) { + return new TestFlow(testTask.thenApply(result -> { + System.out.println( + String.format( + "AssertReply: Starting loop : %s (tid:%s)", + description, + Thread.currentThread().getId() + ) + ); + System.out.flush(); + + int finalTimeout = Integer.MAX_VALUE; + if (isDebug()) + finalTimeout = Integer.MAX_VALUE; + + long start = System.currentTimeMillis(); + while (true) { + long current = System.currentTimeMillis(); + + if ((current - start) > (long) finalTimeout) { + System.out.println("AssertReply: Timeout!\n"); + System.out.flush(); + throw new RuntimeException( + String.format("%d ms Timed out waiting for:'%s'", finalTimeout, description) + ); + } + + // System.out.println("Before GetNextReply\n"); + // System.out.flush(); + + Activity replyActivity = this.adapter.getNextReply(); + // System.out.println("After GetNextReply\n"); + // System.out.flush(); + + if (replyActivity != null) { + System.out.printf( + "AssertReply: Received Reply (tid:%s)", + Thread.currentThread().getId() + ); + System.out.flush(); + System.out.printf( + "\n =============\n From: %s\n To:%s\n Text:%s\n ==========\n", + (replyActivity.getFrom() == null) + ? "No from set" + : replyActivity.getFrom().getName(), + (replyActivity.getRecipient() == null) + ? "No recipient set" + : replyActivity.getRecipient().getName(), + (replyActivity.getText() == null) ? "No Text set" : replyActivity.getText() + ); + System.out.flush(); + + // if we have a reply + validateActivity.accept(replyActivity); + return null; + } else { + System.out.printf( + "AssertReply(tid:%s): Waiting..\n", + Thread.currentThread().getId() + ); + System.out.flush(); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }), this); + } + + // Hack to determine if debugger attached.. + public boolean isDebug() { + for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + if (arg.contains("jdwp=")) { + return true; + } + } + return false; + } + + /** + * @param userSays + * @param expected + * @return + */ + public TestFlow turn(String userSays, String expected, String description, int timeout) { + String result = null; + try { + + result = CompletableFuture.supplyAsync(() -> { // Send the message + + if (userSays == null) + throw new IllegalArgumentException("You have to pass a userSays parameter"); + + System.out.print( + String.format( + "TestTurn(%s): USER SAYS: %s \n", + Thread.currentThread().getId(), + userSays + ) + ); + System.out.flush(); + + try { + this.adapter.sendTextToBot(userSays, this.callback); + return null; + } catch (Exception e) { + return e.getMessage(); + } + + }, ExecutorFactory.getExecutor()).thenApply(arg -> { // Assert Reply + int finalTimeout = Integer.MAX_VALUE; + if (isDebug()) + finalTimeout = Integer.MAX_VALUE; + Function validateActivity = activity -> { + if (activity.getText().equals(expected)) { + System.out.println( + String.format( + "TestTurn(tid:%s): Validated text is: %s", + Thread.currentThread().getId(), + expected + ) + ); + System.out.flush(); + + return "SUCCESS"; + } + System.out.println( + String.format( + "TestTurn(tid:%s): Failed validate text is: %s", + Thread.currentThread().getId(), + expected + ) + ); + System.out.flush(); + + return String.format( + "FAIL: %s received in Activity.text (%s expected)", + activity.getText(), + expected + ); + }; + + System.out.println( + String.format( + "TestTurn(tid:%s): Started receive loop: %s", + Thread.currentThread().getId(), + description + ) + ); + System.out.flush(); + long start = System.currentTimeMillis(); + while (true) { + long current = System.currentTimeMillis(); + + if ((current - start) > (long) finalTimeout) + return String.format( + "TestTurn: %d ms Timed out waiting for:'%s'", + finalTimeout, + description + ); + + Activity replyActivity = this.adapter.getNextReply(); + + if (replyActivity != null) { + // if we have a reply + System.out.println( + String.format( + "TestTurn(tid:%s): Received Reply: %s", + Thread.currentThread().getId(), + String.format( + "\n========\n To:%s\n From:%s\n Msg:%s\n=======", + replyActivity.getRecipient().getName(), + replyActivity.getFrom().getName(), + replyActivity.getText() + ) + ) + ); + System.out.flush(); + return validateActivity.apply(replyActivity); + } else { + System.out.println( + String.format( + "TestTurn(tid:%s): No reply..", + Thread.currentThread().getId() + ) + ); + System.out.flush(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + } + }).get(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } + return this; + + } + + /** + * Say() -> shortcut for .Send(user).AssertReply(Expected) + * + * @param userSays + * @param expected + * @return + */ + public TestFlow test(String userSays, String expected) { + return test(userSays, expected, null, 3000); + } + + public TestFlow test(String userSays, String expected, String description) { + return test(userSays, expected, description, 3000); + } + + public TestFlow test(String userSays, String expected, String description, int timeout) { + if (expected == null) + throw new IllegalArgumentException("expected"); + + return this.send(userSays).assertReply(expected, description, timeout); + } + + /** + * Test() -> shortcut for .Send(user).AssertReply(Expected) + * + * @param userSays + * @param expected + * @return + */ + public TestFlow test(String userSays, Activity expected) { + return test(userSays, expected, null, 3000); + } + + public TestFlow test(String userSays, Activity expected, String description) { + return test(userSays, expected, description, 3000); + } + + public TestFlow test(String userSays, Activity expected, String description, int timeout) { + if (expected == null) + throw new IllegalArgumentException("expected"); + + return this.send(userSays).assertReply(expected, description, timeout); + } + + /** + * Say() -> shortcut for .Send(user).AssertReply(Expected) + * + * @param userSays + * @param expected + * @return + */ + public TestFlow test(String userSays, Consumer expected) { + return test(userSays, expected, null, 3000); + } + + public TestFlow test(String userSays, Consumer expected, String description) { + return test(userSays, expected, description, 3000); + } + + public TestFlow test( + String userSays, + Consumer expected, + String description, + int timeout + ) { + if (expected == null) + throw new IllegalArgumentException("expected"); + + return this.send(userSays).assertReply(expected, description, timeout); + } + + /** + * Assert that reply is one of the candidate responses + * + * @param candidates + * @return + */ + public TestFlow assertReplyOneOf(String[] candidates) { + return assertReplyOneOf(candidates, null, 3000); + } + + public TestFlow assertReplyOneOf(String[] candidates, String description) { + return assertReplyOneOf(candidates, description, 3000); + } + + public TestFlow assertReplyOneOf(String[] candidates, String description, int timeout) { + if (candidates == null) + throw new IllegalArgumentException("candidates"); + + return this.assertReply((reply) -> { + for (String candidate : candidates) { + if (StringUtils.equals(reply.getText(), candidate)) + return; + } + throw new RuntimeException( + String.format( + "%s: Not one of candidates: %s", + description, + String.join("\n ", candidates) + ) + ); + }, description, timeout); + } +} diff --git a/libraries/botbuilder/src/test/java/com/microsoft/bot/builder/base/InterceptorManager.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/InterceptorManager.java similarity index 80% rename from libraries/botbuilder/src/test/java/com/microsoft/bot/builder/base/InterceptorManager.java rename to libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/InterceptorManager.java index 24294b3fb..3be10f4f6 100644 --- a/libraries/botbuilder/src/test/java/com/microsoft/bot/builder/base/InterceptorManager.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/InterceptorManager.java @@ -1,10 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.builder.base; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.common.io.BaseEncoding; import okhttp3.*; -import okhttp3.internal.Util; import okio.Buffer; import okio.BufferedSource; @@ -12,42 +14,47 @@ import java.io.IOException; import java.net.URI; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; +import static java.nio.charset.StandardCharsets.UTF_8; + public class InterceptorManager { private final static String RECORD_FOLDER = "session-records/"; - - private Map textReplacementRules = new HashMap(); + private final String testName; // Stores a map of all the HTTP properties in a session // A state machine ensuring a test is always reset before another one is setup - - protected RecordedData recordedData; - - private final String testName; - private final TestBase.TestMode testMode; + protected RecordedData recordedData; + private Map textReplacementRules = new HashMap(); - private InterceptorManager(String testName, TestBase.TestMode testMode) { + private InterceptorManager( + String testName, + TestBase.TestMode testMode + ) { this.testName = testName; this.testMode = testMode; } - public void addTextReplacementRule(String regex, String replacement) { - textReplacementRules.put(regex, replacement); - } - // factory method - public static InterceptorManager create(String testName, TestBase.TestMode testMode) throws IOException { + public static InterceptorManager create( + String testName, + TestBase.TestMode testMode + ) throws IOException { InterceptorManager interceptorManager = new InterceptorManager(testName, testMode); return interceptorManager; } + public void addTextReplacementRule(String regex, String replacement) { + textReplacementRules.put(regex, replacement); + } + public boolean isRecordMode() { return testMode == TestBase.TestMode.RECORD; } @@ -66,6 +73,7 @@ public Response intercept(Chain chain) throws IOException { return record(chain); } }; + case PLAYBACK: readDataFromFile(); return new Interceptor() { @@ -74,9 +82,10 @@ public Response intercept(Chain chain) throws IOException { return playback(chain); } }; + default: System.out.println("==> Unknown AZURE_TEST_MODE: " + testMode); - }; + } return null; } @@ -85,12 +94,14 @@ public void finalizeInterceptor() throws IOException { case RECORD: writeDataToFile(); break; + case PLAYBACK: // Do nothing break; + default: System.out.println("==> Unknown AZURE_TEST_MODE: " + testMode); - }; + } } private Response record(Interceptor.Chain chain) throws IOException { @@ -110,7 +121,9 @@ private Response record(Interceptor.Chain chain) throws IOException { } networkCallRecord.Method = request.method(); - networkCallRecord.Uri = applyReplacementRule(request.url().toString().replaceAll("\\?$", "")); + networkCallRecord.Uri = applyReplacementRule( + request.url().toString().replaceAll("\\?$", "") + ); networkCallRecord.Body = bodyToString(request); @@ -122,8 +135,10 @@ private Response record(Interceptor.Chain chain) throws IOException { // remove pre-added header if this is a waiting or redirection if (networkCallRecord.Response.get("Body") != null) { - if (networkCallRecord.Response.get("Body").contains("InProgress") - || Integer.parseInt(networkCallRecord.Response.get("StatusCode")) == 307) { + if ( + networkCallRecord.Response.get("Body").contains("InProgress") + || Integer.parseInt(networkCallRecord.Response.get("StatusCode")) == 307 + ) { // Do nothing } else { synchronized (recordedData.getNetworkCallRecords()) { @@ -154,9 +169,14 @@ private Response playback(Interceptor.Chain chain) throws IOException { incomingUrl = removeHost(incomingUrl); NetworkCallRecord networkCallRecord = null; synchronized (recordedData) { - for (Iterator iterator = recordedData.getNetworkCallRecords().iterator(); iterator.hasNext(); ) { + for ( + Iterator iterator = recordedData.getNetworkCallRecords().iterator(); + iterator.hasNext();) { NetworkCallRecord record = iterator.next(); - if (record.Method.equalsIgnoreCase(incomingMethod) && removeHost(record.Uri).equalsIgnoreCase(incomingUrl)) { + if ( + record.Method.equalsIgnoreCase(incomingMethod) + && removeHost(record.Uri).equalsIgnoreCase(incomingUrl) + ) { networkCallRecord = record; iterator.remove(); break; @@ -172,16 +192,19 @@ private Response playback(Interceptor.Chain chain) throws IOException { int recordStatusCode = Integer.parseInt(networkCallRecord.Response.get("StatusCode")); - //Response originalResponse = chain.proceed(request); - //originalResponse.body().close(); + // Response originalResponse = chain.proceed(request); + // originalResponse.body().close(); - Response.Builder responseBuilder = new Response.Builder() - .request(request.newBuilder().build()) - .protocol(Protocol.HTTP_2) - .code(recordStatusCode).message("-"); + Response.Builder responseBuilder = new Response.Builder().request( + request.newBuilder().build() + ).protocol(Protocol.HTTP_2).code(recordStatusCode).message("-"); for (Map.Entry pair : networkCallRecord.Response.entrySet()) { - if (!pair.getKey().equals("StatusCode") && !pair.getKey().equals("Body") && !pair.getKey().equals("Content-Length")) { + if ( + !pair.getKey().equals("StatusCode") + && !pair.getKey().equals("Body") + && !pair.getKey().equals("Content-Length") + ) { String rawHeader = pair.getValue(); for (Map.Entry rule : textReplacementRules.entrySet()) { if (rule.getValue() != null) { @@ -201,20 +224,29 @@ private Response playback(Interceptor.Chain chain) throws IOException { } String rawContentType = networkCallRecord.Response.get("content-type"); - String contentType = rawContentType == null - ? "application/json; charset=utf-8" - : rawContentType; + String contentType = rawContentType == null + ? "application/json; charset=utf-8" + : rawContentType; ResponseBody responseBody; if (contentType.toLowerCase().contains("application/json")) { - responseBody = ResponseBody.create(MediaType.parse(contentType), rawBody.getBytes()); + responseBody = ResponseBody.create( + MediaType.parse(contentType), + rawBody.getBytes() + ); } else { - responseBody = ResponseBody.create(MediaType.parse(contentType), BaseEncoding.base64().decode(rawBody)); + responseBody = ResponseBody.create( + MediaType.parse(contentType), + BaseEncoding.base64().decode(rawBody) + ); } responseBuilder.body(responseBody); - responseBuilder.addHeader("Content-Length", String.valueOf(rawBody.getBytes("UTF-8").length)); + responseBuilder.addHeader( + "Content-Length", + String.valueOf(rawBody.getBytes(StandardCharsets.UTF_8).length) + ); } Response newResponse = responseBuilder.build(); @@ -222,13 +254,19 @@ private Response playback(Interceptor.Chain chain) throws IOException { return newResponse; } - private void extractResponseData(Map responseData, Response response) throws IOException { + private void extractResponseData( + Map responseData, + Response response + ) throws IOException { Map> headers = response.headers().toMultimap(); boolean addedRetryAfter = false; for (Map.Entry> header : headers.entrySet()) { String headerValueToStore = header.getValue().get(0); - if (header.getKey().equalsIgnoreCase("location") || header.getKey().equalsIgnoreCase("azure-asyncoperation")) { + if ( + header.getKey().equalsIgnoreCase("location") + || header.getKey().equalsIgnoreCase("azure-asyncoperation") + ) { headerValueToStore = applyReplacementRule(headerValueToStore); } if (header.getKey().equalsIgnoreCase("retry-after")) { @@ -250,9 +288,8 @@ private void extractResponseData(Map responseData, Response resp if (response.header("Content-Encoding") == null) { String contentType = response.header("Content-Type"); if (contentType != null) { - if (contentType.startsWith("application/json")) - { - content = buffer.readString(Util.UTF_8); + if (contentType.startsWith("application/json")) { + content = buffer.readString(UTF_8); } else { content = BaseEncoding.base64().encode(buffer.readByteArray()); } diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/NetworkCallRecord.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/NetworkCallRecord.java new file mode 100644 index 000000000..8df8783d4 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/NetworkCallRecord.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.base; + +import java.util.Map; + +public class NetworkCallRecord { + public String Method; + public String Uri; + public String Body; + + public Map Headers; + public Map Response; +} diff --git a/libraries/botbuilder/src/test/java/com/microsoft/bot/builder/base/RecordedData.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/RecordedData.java similarity index 84% rename from libraries/botbuilder/src/test/java/com/microsoft/bot/builder/base/RecordedData.java rename to libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/RecordedData.java index f178b9cda..84192596e 100644 --- a/libraries/botbuilder/src/test/java/com/microsoft/bot/builder/base/RecordedData.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/RecordedData.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.builder.base; import java.util.LinkedList; diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/TestBase.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/TestBase.java new file mode 100644 index 000000000..7e0f5b4eb --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/base/TestBase.java @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.base; + +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.restclient.LogLevel; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.ServiceResponseBuilder; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.credentials.TokenCredentials; +import com.microsoft.bot.restclient.interceptors.LoggingInterceptor; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public abstract class TestBase { + + protected final static String ZERO_CLIENT_ID = "00000000-0000-0000-0000-000000000000"; + protected final static String ZERO_CLIENT_SECRET = "00000000000000000000000"; + protected final static String ZERO_USER_ID = "<--dummy-user-id-->"; + protected final static String ZERO_BOT_ID = "<--dummy-bot-id-->"; + protected final static String ZERO_TOKEN = "<--dummy-token-->"; + private static final String PLAYBACK_URI = "http://localhost:1234"; + protected static String hostUri = null; + protected static String clientId = null; + protected static String clientSecret = null; + protected static String userId = null; + protected static String botId = null; + private static TestMode testMode = null; + private final RunCondition runCondition; + @Rule + public TestName testName = new TestName(); + protected InterceptorManager interceptorManager = null; + private PrintStream out; + + protected TestBase() { + this(RunCondition.BOTH); + } + + protected TestBase(RunCondition runCondition) { + this.runCondition = runCondition; + } + + private static void initTestMode() throws IOException { + String azureTestMode = System.getenv("AZURE_TEST_MODE"); + if (azureTestMode != null) { + if (azureTestMode.equalsIgnoreCase("Record")) { + testMode = TestMode.RECORD; + } else if (azureTestMode.equalsIgnoreCase("Playback")) { + testMode = TestMode.PLAYBACK; + } else { + throw new IOException("Unknown AZURE_TEST_MODE: " + azureTestMode); + } + } else { + System.out.print( + "Environment variable 'AZURE_TEST_MODE' has not been set yet. Using 'PLAYBACK' mode." + ); + testMode = TestMode.RECORD; + } + } + + private static void initParams() { + try { + Properties mavenProps = new Properties(); + InputStream in = TestBase.class.getResourceAsStream("/maven.properties"); + if (in == null) { + throw new IOException( + "The file \"maven.properties\" has not been generated yet. Please execute \"mvn compile\" to generate the file." + ); + } + mavenProps.load(in); + + clientId = mavenProps.getProperty("clientId"); + clientSecret = mavenProps.getProperty("clientSecret"); + hostUri = mavenProps.getProperty("hostUrl"); + userId = mavenProps.getProperty("userId"); + botId = mavenProps.getProperty("botId"); + } catch (IOException e) { + clientId = ZERO_CLIENT_ID; + clientSecret = ZERO_CLIENT_SECRET; + hostUri = PLAYBACK_URI; + userId = ZERO_USER_ID; + botId = ZERO_BOT_ID; + } + } + + public static boolean isPlaybackMode() { + if (testMode == null) + try { + initTestMode(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Can't init test mode."); + } + return testMode == TestMode.PLAYBACK; + } + + public static boolean isRecordMode() { + return !isPlaybackMode(); + } + + private static void printThreadInfo(String what) { + long id = Thread.currentThread().getId(); + String name = Thread.currentThread().getName(); + System.out.println(String.format("\n***\n*** [%s:%s] - %s\n***\n", name, id, what)); + } + + @BeforeClass + public static void beforeClass() throws IOException { + printThreadInfo("beforeClass"); + initTestMode(); + initParams(); + } + + private String shouldCancelTest(boolean isPlaybackMode) { + // Determine whether to run the test based on the condition the test has been + // configured with + switch (this.runCondition) { + case MOCK_ONLY: + return (!isPlaybackMode) + ? "Test configured to run only as mocked, not live." + : null; + + case LIVE_ONLY: + return (isPlaybackMode) ? "Test configured to run only as live, not mocked." : null; + + default: + return null; + } + } + + @Before + public void beforeTest() throws IOException { + printThreadInfo(String.format("%s: %s", "beforeTest", testName.getMethodName())); + final String skipMessage = shouldCancelTest(isPlaybackMode()); + Assume.assumeTrue(skipMessage, skipMessage == null); + + interceptorManager = InterceptorManager.create(testName.getMethodName(), testMode); + + ServiceClientCredentials credentials; + RestClient restClient; + + if (isPlaybackMode()) { + credentials = new TokenCredentials(null, ZERO_TOKEN); + restClient = buildRestClient( + new RestClient.Builder().withBaseUrl(hostUri + "/").withSerializerAdapter(new JacksonAdapter()).withResponseBuilderFactory(new ServiceResponseBuilder.Factory()).withCredentials(credentials).withLogLevel(LogLevel.NONE).withNetworkInterceptor(new LoggingInterceptor(LogLevel.BODY_AND_HEADERS)).withInterceptor(interceptorManager.initInterceptor()), true + ); + + out = System.out; + System.setOut(new PrintStream(new OutputStream() { + public void write(int b) { + // DO NOTHING + } + })); + } else { // Record mode + credentials = new MicrosoftAppCredentials(clientId, clientSecret); + restClient = buildRestClient( + new RestClient.Builder().withBaseUrl(hostUri + "/").withSerializerAdapter(new JacksonAdapter()).withResponseBuilderFactory(new ServiceResponseBuilder.Factory()).withCredentials(credentials).withLogLevel(LogLevel.NONE).withReadTimeout(3, TimeUnit.MINUTES).withNetworkInterceptor(new LoggingInterceptor(LogLevel.BODY_AND_HEADERS)).withInterceptor(interceptorManager.initInterceptor()), false + ); + + // interceptorManager.addTextReplacementRule(hostUri, PLAYBACK_URI); + } + initializeClients(restClient, botId, userId); + } + + @After + public void afterTest() throws IOException { + if (shouldCancelTest(isPlaybackMode()) != null) { + return; + } + cleanUpResources(); + interceptorManager.finalizeInterceptor(); + } + + protected void addTextReplacementRule(String from, String to) { + interceptorManager.addTextReplacementRule(from, to); + } + + protected RestClient buildRestClient(RestClient.Builder builder, boolean isMocked) { + return builder.build(); + } + + protected abstract void initializeClients( + RestClient restClient, + String botId, + String userId + ) throws IOException; + + protected abstract void cleanUpResources(); + + protected enum RunCondition { + MOCK_ONLY, + LIVE_ONLY, + BOTH + } + + public enum TestMode { + PLAYBACK, + RECORD + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerBadRequestTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerBadRequestTests.java new file mode 100644 index 000000000..ce68b3c25 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerBadRequestTests.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.builder.SimpleAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.teams.FileConsentCardResponse; +import com.microsoft.bot.schema.teams.FileUploadInfo; +import com.microsoft.bot.schema.teams.MessagingExtensionAction; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class TeamsActivityHandlerBadRequestTests { + @Test + public void TestFileConsentBadAction() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse fileConsentCard = new FileConsentCardResponse(); + fileConsentCard.setAction("this.is.a.bad.action"); + fileConsentCard.setUploadInfo(fileInfo); + activity.setValue(fileConsentCard); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TeamsActivityHandler bot = new TeamsActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 400, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewBadAction() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("this.is.a.bad.action"); + activity.setValue(action); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TeamsActivityHandler bot = new TeamsActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 400, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerHidingTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerHidingTests.java new file mode 100644 index 000000000..8a6dec90d --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerHidingTests.java @@ -0,0 +1,387 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.MessageReaction; +import com.microsoft.bot.schema.ResourceResponse; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * These are the same tests as are performed on direct subclasses of + * ActivityHandler but this time the test bot is derived from + * TeamsActivityHandler. + */ +public class TeamsActivityHandlerHidingTests { + @Test + public void TestMessageActivity() { + Activity activity = MessageFactory.text("hello"); + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onMessageActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberAdded1() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("b")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberAdded2() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("b")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersAdded", bot.getRecord().get(1)); + } + + @Test + public void TestMemberAdded3() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("b")); + members.add(new ChannelAccount("c")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersAdded", bot.getRecord().get(1)); + } + + @Test + public void TestMemberRemoved1() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberRemoved2() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersRemoved", bot.getRecord().get(1)); + } + + @Test + public void TestMemberRemoved3() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + members.add(new ChannelAccount("b")); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + Assert.assertEquals("onMembersRemoved", bot.getRecord().get(1)); + } + + @Test + public void TestMemberAddedJustTheBot() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("b")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMemberRemovedJustTheBot() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("c")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("c")); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onConversationUpdateActivity", bot.getRecord().get(0)); + } + + @Test + public void TestMessageReaction() { + // Note the code supports multiple adds and removes in the same activity though + // a channel may decide to send separate activities for each. For example, Teams + // sends separate activities each with a single add and a single remove. + + // Arrange + Activity activity = new Activity(ActivityTypes.MESSAGE_REACTION); + ArrayList reactionsAdded = new ArrayList(); + reactionsAdded.add(new MessageReaction("sad")); + activity.setReactionsAdded(reactionsAdded); + ArrayList reactionsRemoved = new ArrayList(); + reactionsRemoved.add(new MessageReaction("angry")); + activity.setReactionsRemoved(reactionsRemoved); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.getRecord().size()); + Assert.assertEquals("onMessageReactionActivity", bot.getRecord().get(0)); + Assert.assertEquals("onReactionsAdded", bot.getRecord().get(1)); + Assert.assertEquals("onReactionsRemoved", bot.getRecord().get(2)); + } + + @Test + public void TestTokenResponseEventAsync() { + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setName("tokens/response"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onEventActivity", bot.getRecord().get(0)); + Assert.assertEquals("onTokenResponseEvent", bot.getRecord().get(1)); + } + + @Test + public void TestEventAsync() { + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setName("some.random.event"); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onEventActivity", bot.getRecord().get(0)); + Assert.assertEquals("onEvent", bot.getRecord().get(1)); + } + + @Test + public void TestEventNullNameAsync() { + Activity activity = new Activity(ActivityTypes.EVENT); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.getRecord().size()); + Assert.assertEquals("onEventActivity", bot.getRecord().get(0)); + Assert.assertEquals("onEvent", bot.getRecord().get(1)); + } + + @Test + public void TestUnrecognizedActivityType() { + Activity activity = new Activity("shall.not.pass"); + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(1, bot.getRecord().size()); + Assert.assertEquals("onUnrecognizedActivityType", bot.getRecord().get(0)); + } + + private static class NotImplementedAdapter extends BotAdapter { + @Override + public CompletableFuture sendActivities( + TurnContext context, + List activities + ) { + return Async.completeExceptionally(new RuntimeException()); + } + + @Override + public CompletableFuture updateActivity( + TurnContext context, + Activity activity + ) { + return Async.completeExceptionally(new RuntimeException()); + } + + @Override + public CompletableFuture deleteActivity( + TurnContext context, + ConversationReference reference + ) { + return Async.completeExceptionally(new RuntimeException()); + } + } + + private static class TestActivityHandler extends TeamsActivityHandler { + private List record = new ArrayList<>(); + + public List getRecord() { + return record; + } + + public void setRecord(List record) { + this.record = record; + } + + @Override + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + record.add("onMessageActivity"); + return super.onMessageActivity(turnContext); + } + + @Override + protected CompletableFuture onConversationUpdateActivity(TurnContext turnContext) { + record.add("onConversationUpdateActivity"); + return super.onConversationUpdateActivity(turnContext); + } + + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, + TurnContext turnContext + ) { + record.add("onMembersAdded"); + return super.onMembersAdded(membersAdded, turnContext); + } + + @Override + protected CompletableFuture onMembersRemoved( + List membersRemoved, + TurnContext turnContext + ) { + record.add("onMembersRemoved"); + return super.onMembersRemoved(membersRemoved, turnContext); + } + + @Override + protected CompletableFuture onMessageReactionActivity(TurnContext turnContext) { + record.add("onMessageReactionActivity"); + return super.onMessageReactionActivity(turnContext); + } + + @Override + protected CompletableFuture onReactionsAdded( + List messageReactions, + TurnContext turnContext + ) { + record.add("onReactionsAdded"); + return super.onReactionsAdded(messageReactions, turnContext); + } + + @Override + protected CompletableFuture onReactionsRemoved( + List messageReactions, + TurnContext turnContext + ) { + record.add("onReactionsRemoved"); + return super.onReactionsRemoved(messageReactions, turnContext); + } + + @Override + protected CompletableFuture onEventActivity(TurnContext turnContext) { + record.add("onEventActivity"); + return super.onEventActivity(turnContext); + } + + @Override + protected CompletableFuture onTokenResponseEvent(TurnContext turnContext) { + record.add("onTokenResponseEvent"); + return super.onTokenResponseEvent(turnContext); + } + + @Override + protected CompletableFuture onEvent(TurnContext turnContext) { + record.add("onEvent"); + return super.onEvent(turnContext); + } + + @Override + protected CompletableFuture onUnrecognizedActivityType(TurnContext turnContext) { + record.add("onUnrecognizedActivityType"); + return super.onUnrecognizedActivityType(turnContext); + } + + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerNotImplementedTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerNotImplementedTests.java new file mode 100644 index 000000000..aea8885e4 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerNotImplementedTests.java @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.builder.SimpleAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.teams.AppBasedLinkQuery; +import com.microsoft.bot.schema.teams.FileConsentCardResponse; +import com.microsoft.bot.schema.teams.FileUploadInfo; +import com.microsoft.bot.schema.teams.MessagingExtensionAction; +import com.microsoft.bot.schema.teams.MessagingExtensionActionResponse; +import com.microsoft.bot.schema.teams.MessagingExtensionQuery; +import com.microsoft.bot.schema.teams.O365ConnectorCardActionQuery; +import com.microsoft.bot.schema.teams.TaskModuleRequest; +import com.microsoft.bot.schema.teams.TaskModuleRequestContext; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +public class TeamsActivityHandlerNotImplementedTests { + @Test + public void TestInvoke() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("gibberish"); + + assertNotImplemented(activity); + } + + @Test + public void TestFileConsentAccept() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse response = new FileConsentCardResponse(); + response.setAction("accept"); + response.setUploadInfo(fileInfo); + activity.setValue(response); + + assertNotImplemented(activity); + } + + @Test + public void TestFileConsentDecline() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse response = new FileConsentCardResponse(); + response.setAction("decline"); + response.setUploadInfo(fileInfo); + activity.setValue(response); + + assertNotImplemented(activity); + } + + @Test + public void TestActionableMessageExecuteAction() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("actionableMessage/executeAction"); + activity.setValue(new O365ConnectorCardActionQuery()); + + assertNotImplemented(activity); + } + + @Test + public void TestComposeExtensionQueryLink() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/queryLink"); + activity.setValue(new AppBasedLinkQuery()); + + assertNotImplemented(activity); + } + + @Test + public void TestComposeExtensionQuery() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/query"); + activity.setValue(new MessagingExtensionQuery()); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionSelectItem() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/selectItem"); + activity.setValue(new O365ConnectorCardActionQuery()); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionSubmitAction() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + activity.setValue(new MessagingExtensionQuery()); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewActionEdit() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("edit"); + activity.setValue(action); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewActionSend() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("send"); + activity.setValue(action); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionFetchTask() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/fetchTask"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setCommandId("testCommand"); + activity.setValue(action); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionConfigurationQuerySettingsUrl() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/querySettingsUrl"); + + assertNotImplemented(activity); + } + + @Test + public void TestMessagingExtensionConfigurationSetting() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/setting"); + + assertNotImplemented(activity); + } + + @Test + public void TestTaskModuleFetch() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("task/fetch"); + HashMap data = new HashMap(); + data.put("key", "value"); + data.put("type", "task / fetch"); + TaskModuleRequestContext context = new TaskModuleRequestContext(); + context.setTheme("default"); + TaskModuleRequest request = new TaskModuleRequest(); + request.setData(data); + request.setContext(context); + activity.setValue(request); + assertNotImplemented(activity); + } + + @Test + public void TestTaskModuleSubmit() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("task/submit"); + HashMap data = new HashMap(); + data.put("key", "value"); + data.put("type", "task / fetch"); + TaskModuleRequestContext context = new TaskModuleRequestContext(); + context.setTheme("default"); + TaskModuleRequest request = new TaskModuleRequest(); + request.setData(data); + request.setContext(context); + activity.setValue(request); + assertNotImplemented(activity); + } + + @Test + public void TestFileConsentAcceptImplemented() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse response = new FileConsentCardResponse(); + response.setAction("accept"); + response.setUploadInfo(fileInfo); + activity.setValue(response); + + assertImplemented(activity, new TestActivityHandlerFileConsent()); + } + + @Test + public void TestFileConsentDeclineImplemented() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse response = new FileConsentCardResponse(); + response.setAction("decline"); + response.setUploadInfo(fileInfo); + activity.setValue(response); + + assertImplemented(activity, new TestActivityHandlerFileConsent()); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewActionEditImplemented() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("edit"); + activity.setValue(action); + + assertImplemented(activity, new TestActivityHandlerPrevieAction()); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewActionSendImplemented() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("send"); + activity.setValue(action); + + assertImplemented(activity, new TestActivityHandlerPrevieAction()); + } + + private void assertNotImplemented(Activity activity) { + assertInvokeResponse(activity, new TestActivityHandler(), 501); + } + + private void assertImplemented(Activity activity, ActivityHandler bot) { + assertInvokeResponse(activity, bot, 200); + } + + private void assertInvokeResponse(Activity activity, ActivityHandler bot, int expectedStatus) { + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + bot.onTurn(turnContext).join(); + + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + expectedStatus, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + private static class TestActivityHandler extends TeamsActivityHandler { + + } + + private static class TestActivityHandlerFileConsent extends TeamsActivityHandler { + @Override + protected CompletableFuture onTeamsFileConsentAccept( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsFileConsentDecline( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + return CompletableFuture.completedFuture(null); + } + } + + private static class TestActivityHandlerPrevieAction extends TeamsActivityHandler { + + @Override + protected CompletableFuture onTeamsMessagingExtensionBotMessagePreviewEdit( + TurnContext turnContext, + MessagingExtensionAction action + ) { + return CompletableFuture.completedFuture(new MessagingExtensionActionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionBotMessagePreviewSend( + TurnContext turnContext, + MessagingExtensionAction action + ) { + return CompletableFuture.completedFuture(new MessagingExtensionActionResponse()); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerTests.java new file mode 100644 index 000000000..ab813f3fb --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsActivityHandlerTests.java @@ -0,0 +1,1425 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotFrameworkAdapter; +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.builder.SimpleAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.teams.AppBasedLinkQuery; +import com.microsoft.bot.schema.teams.ChannelInfo; +import com.microsoft.bot.schema.teams.FileConsentCardResponse; +import com.microsoft.bot.schema.teams.FileUploadInfo; +import com.microsoft.bot.schema.teams.MeetingEndEventDetails; +import com.microsoft.bot.schema.teams.MeetingStartEventDetails; +import com.microsoft.bot.schema.teams.MessagingExtensionAction; +import com.microsoft.bot.schema.teams.MessagingExtensionActionResponse; +import com.microsoft.bot.schema.teams.MessagingExtensionQuery; +import com.microsoft.bot.schema.teams.MessagingExtensionResponse; +import com.microsoft.bot.schema.teams.O365ConnectorCardActionQuery; +import com.microsoft.bot.schema.teams.TaskModuleRequest; +import com.microsoft.bot.schema.teams.TaskModuleRequestContext; +import com.microsoft.bot.schema.teams.TaskModuleResponse; +import com.microsoft.bot.schema.teams.TeamInfo; +import com.microsoft.bot.schema.teams.TeamsChannelAccount; +import com.microsoft.bot.schema.teams.TeamsChannelData; +import java.io.IOException; +import org.apache.commons.lang3.NotImplementedException; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +public class TeamsActivityHandlerTests { + @Test + public void TestConversationUpdateBotTeamsMemberAdded() { + String baseUri = "https://test.coffee"; + ConnectorClient connectorClient = getConnectorClient( + "http://localhost/", + MicrosoftAppCredentials.empty() + ); + + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("botid-1")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("botid-1")); + TeamsChannelData channelData = new TeamsChannelData(); + channelData.setEventType("teamMemberAdded"); + channelData.setTeam(new TeamInfo("team-id")); + activity.setChannelData(channelData); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMembersAdded", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsMemberAdded() { + String baseUri = "https://test.coffee"; + ConnectorClient connectorClient = getConnectorClient( + "http://localhost/", + MicrosoftAppCredentials.empty() + ); + + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("id-1")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + TeamsChannelData channelData = new TeamsChannelData(); + channelData.setEventType("teamMemberAdded"); + channelData.setTeam(new TeamInfo("team-id")); + activity.setChannelData(channelData); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMembersAdded", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsMemberAddedNoTeam() { + String baseUri = "https://test.coffee"; + ConnectorClient connectorClient = getConnectorClient( + "http://localhost/", + MicrosoftAppCredentials.empty() + ); + + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("id-1")); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + activity.setConversation(new ConversationAccount("conversation-id")); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMembersAdded", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsMemberAddedFullDetailsInEvent() { + String baseUri = "https://test.coffee"; + ConnectorClient connectorClient = getConnectorClient( + "http://localhost/", + MicrosoftAppCredentials.empty() + ); + + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + TeamsChannelAccount teams = new TeamsChannelAccount(); + teams.setId("id-1"); + teams.setName("name-1"); + teams.setAadObjectId("aadobject-1"); + teams.setEmail("test@microsoft.com"); + teams.setGivenName("given-1"); + teams.setSurname("surname-1"); + teams.setUserPrincipalName("t@microsoft.com"); + teams.setTenantId("testTenantId"); + teams.setUserRole("guest"); + members.add(teams); + activity.setMembersAdded(members); + activity.setRecipient(new ChannelAccount("b")); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamMemberAdded"); + data.setTeam(new TeamInfo("team-id")); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + // serialize to json and back to verify we can get back to the + // correct Activity. i.e., In this case, mainly the TeamsChannelAccount. + try { + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + String json = jacksonAdapter.serialize(activity); + activity = jacksonAdapter.deserialize(json, Activity.class); + } catch (Throwable t) { + Assert.fail("Should not have thrown in serialization test."); + } + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMembersAdded", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsMemberRemoved() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + ArrayList members = new ArrayList(); + members.add(new ChannelAccount("a")); + activity.setMembersRemoved(members); + activity.setRecipient(new ChannelAccount("b")); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamMemberRemoved"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMembersRemoved", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsChannelCreated() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("channelCreated"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsChannelCreated", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsChannelDeleted() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("channelDeleted"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsChannelDeleted", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsChannelRenamed() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("channelRenamed"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsChannelRenamed", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsChannelRestored() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("channelRestored"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsChannelRestored", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsTeamRenamed() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamRenamed"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTeamRenamed", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsTeamArchived() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamArchived"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTeamArchived", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsTeamDeleted() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamDeleted"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTeamDeleted", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsTeamHardDeleted() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamHardDeleted"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTeamHardDeleted", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsTeamRestored() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamRestored"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTeamRestored", bot.record.get(1)); + } + + @Test + public void TestConversationUpdateTeamsTeamUnarchived() { + Activity activity = new Activity(ActivityTypes.CONVERSATION_UPDATE); + TeamsChannelData data = new TeamsChannelData(); + data.setEventType("teamUnarchived"); + activity.setChannelData(data); + activity.setChannelId(Channels.MSTEAMS); + + TurnContext turnContext = new TurnContextImpl(new NotImplementedAdapter(), activity); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onConversationUpdateActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTeamUnarchived", bot.record.get(1)); + } + + @Test + public void TestFileConsentAccept() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse response = new FileConsentCardResponse(); + response.setAction("accept"); + response.setUploadInfo(fileInfo); + activity.setValue(response); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsFileConsent", bot.record.get(1)); + Assert.assertEquals("onTeamsFileConsentAccept", bot.record.get(2)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestFileConsentDecline() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("fileConsent/invoke"); + FileUploadInfo fileInfo = new FileUploadInfo(); + fileInfo.setUniqueId("uniqueId"); + fileInfo.setFileType("fileType"); + fileInfo.setUploadUrl("uploadUrl"); + FileConsentCardResponse response = new FileConsentCardResponse(); + response.setAction("decline"); + response.setUploadInfo(fileInfo); + activity.setValue(response); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsFileConsent", bot.record.get(1)); + Assert.assertEquals("onTeamsFileConsentDecline", bot.record.get(2)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestActionableMessageExecuteAction() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("actionableMessage/executeAction"); + activity.setValue(new O365ConnectorCardActionQuery()); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsO365ConnectorCardAction", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestComposeExtensionQueryLink() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/queryLink"); + activity.setValue(new AppBasedLinkQuery()); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsAppBasedLinkQuery", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestComposeExtensionQuery() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/query"); + activity.setValue(new MessagingExtensionQuery()); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionQuery", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionSelectItemAsync() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/selectItem"); + activity.setValue(new MessagingExtensionQuery()); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionSelectItem", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionSubmitAction() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + activity.setValue(new MessagingExtensionQuery()); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionSubmitActionDispatch", bot.record.get(1)); + Assert.assertEquals("onTeamsMessagingExtensionSubmitAction", bot.record.get(2)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewActionEdit() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("edit"); + activity.setValue(action); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionSubmitActionDispatch", bot.record.get(1)); + Assert.assertEquals("onTeamsMessagingExtensionBotMessagePreviewEdit", bot.record.get(2)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionSubmitActionPreviewActionSend() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/submitAction"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setBotMessagePreviewAction("send"); + activity.setValue(action); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(3, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionSubmitActionDispatch", bot.record.get(1)); + Assert.assertEquals("onTeamsMessagingExtensionBotMessagePreviewSend", bot.record.get(2)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionFetchTask() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/fetchTask"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setCommandId("testCommand"); + activity.setValue(action); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionFetchTask", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionConfigurationQuerySettingUrl() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/querySettingUrl"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setCommandId("testCommand"); + activity.setValue(action); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals( + "onTeamsMessagingExtensionConfigurationQuerySettingUrl", + bot.record.get(1) + ); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestMessagingExtensionConfigurationSetting() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("composeExtension/setting"); + MessagingExtensionAction action = new MessagingExtensionAction(); + action.setCommandId("testCommand"); + activity.setValue(action); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMessagingExtensionConfigurationSetting", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestTaskModuleFetch() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("task/fetch"); + TaskModuleRequestContext context = new TaskModuleRequestContext(); + context.setTheme("default"); + HashMap data = new HashMap(); + data.put("key", "value"); + data.put("type", "task / fetch"); + TaskModuleRequest request = new TaskModuleRequest(); + request.setData(data); + request.setContext(context); + activity.setValue(request); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTaskModuleFetch", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestTaskModuleSubmit() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("task/submit"); + TaskModuleRequestContext context = new TaskModuleRequestContext(); + context.setTheme("default"); + HashMap data = new HashMap(); + data.put("key", "value"); + data.put("type", "task / fetch"); + TaskModuleRequest request = new TaskModuleRequest(); + request.setData(data); + request.setContext(context); + activity.setValue(request); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsTaskModuleSubmit", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestSigninVerifyState() { + Activity activity = new Activity(ActivityTypes.INVOKE); + activity.setName("signin/verifyState"); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onInvokeActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsSigninVerifyState", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getValue() instanceof InvokeResponse); + Assert.assertEquals( + 200, + ((InvokeResponse) activitiesToSend.get().get(0).getValue()).getStatus() + ); + } + + @Test + public void TestOnEventActivity() { + // Arrange + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setChannelId(Channels.DIRECTLINE); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + + // Act + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + // Assert + Assert.assertEquals(1, bot.record.size()); + Assert.assertEquals("onEventActivity", bot.record.get(0)); + } + + @Test + public void TestMeetingStartEvent() throws IOException { + // Arrange + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setChannelId(Channels.MSTEAMS); + activity.setName("application/vnd.microsoft.meetingStart"); + activity.setValue(Serialization.jsonToTree("{\"StartTime\": \"2021-06-05T00:01:02.0Z\"}")); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + // Act + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + // Assert + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onEventActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMeetingStart", bot.record.get(1)); + + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getText().contains("00:01:02")); + } + + @Test + public void TestMeetingEndEvent() throws IOException { + // Arrange + Activity activity = new Activity(ActivityTypes.EVENT); + activity.setChannelId(Channels.MSTEAMS); + activity.setName("application/vnd.microsoft.meetingEnd"); + activity.setValue(Serialization.jsonToTree("{\"EndTime\": \"2021-06-05T01:02:03.0Z\"}")); + + AtomicReference> activitiesToSend = new AtomicReference<>(); + + TurnContext turnContext = new TurnContextImpl( + new SimpleAdapter(activitiesToSend::set), + activity + ); + + // Act + TestActivityHandler bot = new TestActivityHandler(); + bot.onTurn(turnContext).join(); + + // Assert + Assert.assertEquals(2, bot.record.size()); + Assert.assertEquals("onEventActivity", bot.record.get(0)); + Assert.assertEquals("onTeamsMeetingEnd", bot.record.get(1)); + Assert.assertNotNull(activitiesToSend.get()); + Assert.assertEquals(1, activitiesToSend.get().size()); + Assert.assertTrue(activitiesToSend.get().get(0).getText().contains("1:02:03")); + } + + private static class NotImplementedAdapter extends BotAdapter { + + @Override + public CompletableFuture sendActivities( + TurnContext context, + List activities + ) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new NotImplementedException("sendActivities")); + return result; + } + + @Override + public CompletableFuture updateActivity( + TurnContext context, + Activity activity + ) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new NotImplementedException("updateActivity")); + return result; + } + + @Override + public CompletableFuture deleteActivity( + TurnContext context, + ConversationReference reference + ) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new NotImplementedException("deleteActivity")); + return result; + } + } + + private static class TestActivityHandler extends TeamsActivityHandler { + public List record = new ArrayList<>(); + + @Override + protected CompletableFuture onInvokeActivity(TurnContext turnContext) { + record.add("onInvokeActivity"); + return super.onInvokeActivity(turnContext); + } + + @Override + protected CompletableFuture onTeamsCardActionInvoke( + TurnContext turnContext + ) { + record.add("onTeamsCardActionInvoke"); + return super.onTeamsCardActionInvoke(turnContext); + } + + @Override + protected CompletableFuture onTeamsSigninVerifyState(TurnContext turnContext) { + record.add("onTeamsSigninVerifyState"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsFileConsent( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + record.add("onTeamsFileConsent"); + return super.onTeamsFileConsent(turnContext, fileConsentCardResponse); + } + + @Override + protected CompletableFuture onTeamsFileConsentAccept( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + record.add("onTeamsFileConsentAccept"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsFileConsentDecline( + TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse + ) { + record.add("onTeamsFileConsentDecline"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionQuery( + TurnContext turnContext, + MessagingExtensionQuery query + ) { + record.add("onTeamsMessagingExtensionQuery"); + return CompletableFuture.completedFuture(new MessagingExtensionResponse()); + } + + @Override + protected CompletableFuture onTeamsO365ConnectorCardAction( + TurnContext turnContext, + O365ConnectorCardActionQuery query + ) { + record.add("onTeamsO365ConnectorCardAction"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsAppBasedLinkQuery( + TurnContext turnContext, + AppBasedLinkQuery query + ) { + record.add("onTeamsAppBasedLinkQuery"); + return CompletableFuture.completedFuture(new MessagingExtensionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionSelectItem( + TurnContext turnContext, + Object query + ) { + record.add("onTeamsMessagingExtensionSelectItem"); + return CompletableFuture.completedFuture(new MessagingExtensionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionFetchTask( + TurnContext turnContext, + MessagingExtensionAction action + ) { + record.add("onTeamsMessagingExtensionFetchTask"); + return CompletableFuture.completedFuture(new MessagingExtensionActionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionSubmitActionDispatch( + TurnContext turnContext, + MessagingExtensionAction action + ) { + record.add("onTeamsMessagingExtensionSubmitActionDispatch"); + return super.onTeamsMessagingExtensionSubmitActionDispatch(turnContext, action); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionSubmitAction( + TurnContext turnContext, + MessagingExtensionAction action + ) { + record.add("onTeamsMessagingExtensionSubmitAction"); + return CompletableFuture.completedFuture(new MessagingExtensionActionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionBotMessagePreviewEdit( + TurnContext turnContext, + MessagingExtensionAction action + ) { + record.add("onTeamsMessagingExtensionBotMessagePreviewEdit"); + return CompletableFuture.completedFuture(new MessagingExtensionActionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionBotMessagePreviewSend( + TurnContext turnContext, + MessagingExtensionAction action + ) { + record.add("onTeamsMessagingExtensionBotMessagePreviewSend"); + return CompletableFuture.completedFuture(new MessagingExtensionActionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionConfigurationQuerySettingUrl( + TurnContext turnContext, + MessagingExtensionQuery query + ) { + record.add("onTeamsMessagingExtensionConfigurationQuerySettingUrl"); + return CompletableFuture.completedFuture(new MessagingExtensionResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionConfigurationSetting( + TurnContext turnContext, + Object settings + ) { + record.add("onTeamsMessagingExtensionConfigurationSetting"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsTaskModuleFetch( + TurnContext turnContext, + TaskModuleRequest taskModuleRequest + ) { + record.add("onTeamsTaskModuleFetch"); + return CompletableFuture.completedFuture(new TaskModuleResponse()); + } + + @Override + protected CompletableFuture onTeamsMessagingExtensionCardButtonClicked( + TurnContext turnContext, + Object cardData + ) { + record.add("onTeamsMessagingExtensionCardButtonClicked"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsTaskModuleSubmit( + TurnContext turnContext, + TaskModuleRequest taskModuleRequest + ) { + record.add("onTeamsTaskModuleSubmit"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onConversationUpdateActivity(TurnContext turnContext) { + record.add("onConversationUpdateActivity"); + return super.onConversationUpdateActivity(turnContext); + } + + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, + TurnContext turnContext + ) { + record.add("onMembersAdded"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onMembersRemoved( + List membersRemoved, + TurnContext turnContext + ) { + record.add("onMembersRemoved"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsMembersAdded( + List membersAdded, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsMembersAdded"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsMembersRemoved( + List membersRemoved, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsMembersRemoved"); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture onTeamsChannelCreated( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsChannelCreated"); + return super.onTeamsChannelCreated(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsChannelDeleted( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsChannelDeleted"); + return super.onTeamsChannelDeleted(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsChannelRenamed( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsChannelRenamed"); + return super.onTeamsChannelRenamed(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsChannelRestored( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsChannelRestored"); + return super.onTeamsChannelRestored(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsTeamRenamed( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsTeamRenamed"); + return super.onTeamsTeamRenamed(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsTeamArchived( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsTeamArchived"); + return super.onTeamsTeamArchived(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsTeamDeleted( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsTeamDeleted"); + return super.onTeamsTeamDeleted(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsTeamHardDeleted( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsTeamHardDeleted"); + return super.onTeamsTeamHardDeleted(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsTeamRestored( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsTeamRestored"); + return super.onTeamsTeamRestored(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onTeamsTeamUnarchived( + ChannelInfo channelInfo, + TeamInfo teamInfo, + TurnContext turnContext + ) { + record.add("onTeamsTeamUnarchived"); + return super.onTeamsTeamUnarchived(channelInfo, teamInfo, turnContext); + } + + @Override + protected CompletableFuture onEventActivity( + TurnContext turnContext + ) { + record.add("onEventActivity"); + return super.onEventActivity(turnContext); + } + + @Override + protected CompletableFuture onTeamsMeetingStart( + MeetingStartEventDetails meeting, + TurnContext turnContext + ) { + record.add("onTeamsMeetingStart"); + return turnContext.sendActivity(meeting.getStartTime().toString()) + .thenCompose(resourceResponse -> super.onTeamsMeetingStart(meeting, turnContext)); + } + + @Override + protected CompletableFuture onTeamsMeetingEnd( + MeetingEndEventDetails meeting, + TurnContext turnContext + ) { + record.add("onTeamsMeetingEnd"); + return turnContext.sendActivity(meeting.getEndTime().toString()) + .thenCompose(resourceResponse -> super.onTeamsMeetingEnd(meeting, turnContext)); + } + } + + private static ConnectorClient getConnectorClient(String baseUri, AppCredentials credentials) { + Conversations mockConversations = Mockito.mock(Conversations.class); + + ConversationResourceResponse response = new ConversationResourceResponse(); + response.setId("team-id"); + response.setServiceUrl("https://serviceUrl/"); + response.setActivityId("activityId123"); + + // createConversation + Mockito.when( + mockConversations.createConversation(Mockito.any(ConversationParameters.class)) + ).thenReturn(CompletableFuture.completedFuture(response)); + + + ArrayList channelAccount1 = new ArrayList(); + ChannelAccount account1 = new ChannelAccount(); + account1.setId("id-1"); + account1.setName("name-1"); + account1.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-1")); + account1.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-1")); + account1.setProperties("surname", JsonNodeFactory.instance.textNode("surname-1")); + account1.setProperties("email", JsonNodeFactory.instance.textNode("email-1")); + account1.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-1")); + account1.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-1")); + channelAccount1.add(account1); + + ChannelAccount account2 = new ChannelAccount(); + account2.setId("id-2"); + account2.setName("name-2"); + account2.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-2")); + account2.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-2")); + account2.setProperties("surname",JsonNodeFactory.instance.textNode("surname-2")); + account2.setProperties("email", JsonNodeFactory.instance.textNode("email-2")); + account2.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-2")); + account2.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-2")); + channelAccount1.add(account2); + // getConversationMembers (Team) + Mockito.when(mockConversations.getConversationMembers("team-id")).thenReturn( + CompletableFuture.completedFuture(channelAccount1) + ); + + + ArrayList channelAccount2 = new ArrayList(); + ChannelAccount channelAccount3 = new ChannelAccount(); + channelAccount3.setId("id-3"); + channelAccount3.setName("name-3"); + channelAccount3.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-3")); + channelAccount3.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-3")); + channelAccount3.setProperties("surname", JsonNodeFactory.instance.textNode("surname-3")); + channelAccount3.setProperties("email", JsonNodeFactory.instance.textNode("email-3")); + channelAccount3.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-3")); + channelAccount3.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-3")); + channelAccount2.add(channelAccount3); + ChannelAccount channelAccount4 = new ChannelAccount(); + channelAccount4.setId("id-4"); + channelAccount4.setName("name-4"); + channelAccount4.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-4")); + channelAccount4.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-4")); + channelAccount4.setProperties("surname", JsonNodeFactory.instance.textNode("surname-4")); + channelAccount4.setProperties("email", JsonNodeFactory.instance.textNode("email-4")); + channelAccount4.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-4")); + channelAccount4.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-4")); + channelAccount2.add(channelAccount4); + // getConversationMembers (Group chat) + Mockito.when(mockConversations.getConversationMembers("conversation-id")).thenReturn( + CompletableFuture.completedFuture(channelAccount2) + ); + + + ChannelAccount channelAccount5 = new ChannelAccount(); + channelAccount5.setId("id-1"); + channelAccount5.setName("name-1"); + channelAccount5.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-1")); + channelAccount5.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-1")); + channelAccount5.setProperties("surname", JsonNodeFactory.instance.textNode("surname-1")); + channelAccount5.setProperties("email", JsonNodeFactory.instance.textNode("email-1")); + channelAccount5.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-1")); + channelAccount5.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-1")); + // getConversationMember (Team) + Mockito.when(mockConversations.getConversationMember("id-1", "team-id")).thenReturn( + CompletableFuture.completedFuture(channelAccount5) + ); + + + ChannelAccount channelAccount6 = new ChannelAccount(); + channelAccount6.setId("id-1"); + channelAccount6.setName("name-1"); + channelAccount6.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-1")); + channelAccount6.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-1")); + channelAccount6.setProperties("surname", JsonNodeFactory.instance.textNode("surname-1")); + channelAccount6.setProperties("email", JsonNodeFactory.instance.textNode("email-1")); + channelAccount6.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-1")); + channelAccount6.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-1")); + // getConversationMember (Group chat) + Mockito.when(mockConversations.getConversationMember("id-1", "conversation-id")).thenReturn( + CompletableFuture.completedFuture(channelAccount6) + ); + + ConnectorClient mockConnectorClient = Mockito.mock(ConnectorClient.class); + Mockito.when(mockConnectorClient.getConversations()).thenReturn(mockConversations); + Mockito.when(mockConnectorClient.baseUrl()).thenReturn(baseUri); + Mockito.when(mockConnectorClient.credentials()).thenReturn(credentials); + + return mockConnectorClient; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java new file mode 100644 index 000000000..942590aa8 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java @@ -0,0 +1,481 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.teams; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotFrameworkAdapter; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.SimpleAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.CredentialProvider; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; +import com.microsoft.bot.connector.teams.TeamsConnectorClient; +import com.microsoft.bot.connector.teams.TeamsOperations; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.Pair; +import com.microsoft.bot.schema.teams.ChannelInfo; +import com.microsoft.bot.schema.teams.ConversationList; +import com.microsoft.bot.schema.teams.MeetingDetails; +import com.microsoft.bot.schema.teams.MeetingInfo; +import com.microsoft.bot.schema.teams.TeamDetails; +import com.microsoft.bot.schema.teams.TeamInfo; +import com.microsoft.bot.schema.teams.TeamsChannelAccount; +import com.microsoft.bot.schema.teams.TeamsChannelData; +import com.microsoft.bot.schema.teams.TeamsMeetingInfo; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TeamsInfoTests { + @Test + public void TestSendMessageToTeamsChannel() { + String baseUri = "https://test.coffee"; + MicrosoftAppCredentials credentials = new MicrosoftAppCredentials( + "big-guid-here", + "appPasswordHere" + ); + ConnectorClient connectorClient = getConnectorClient(baseUri, credentials); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-SendMessageToTeamsChannelAsync"); + activity.setChannelId(Channels.MSTEAMS); + TeamsChannelData data = new TeamsChannelData(); + data.setTeam(new TeamInfo("team-id")); + activity.setChannelData(data); + + TurnContext turnContext = new TurnContextImpl( + new TestBotFrameworkAdapter( + new SimpleCredentialProvider("big-guid-here", "appPasswordHere") + ), + activity + ); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY, + getTeamsConnectorClient(connectorClient.baseUrl(), credentials) + ); + turnContext.getActivity().setServiceUrl("https://test.coffee"); + + ActivityHandler handler = new TestTeamsActivityHandler(); + handler.onTurn(turnContext).join(); + } + + @Test + public void TestGetTeamDetails() { + String baseUri = "https://test.coffee"; + MicrosoftAppCredentials credentials = MicrosoftAppCredentials.empty(); + ConnectorClient connectorClient = getConnectorClient(baseUri, credentials); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-GetTeamDetailsAsync"); + activity.setChannelId(Channels.MSTEAMS); + TeamsChannelData data = new TeamsChannelData(); + data.setTeam(new TeamInfo("team-id")); + activity.setChannelData(data); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY, + getTeamsConnectorClient(connectorClient.baseUrl(), credentials) + ); + turnContext.getActivity().setServiceUrl("https://test.coffee"); + + ActivityHandler handler = new TestTeamsActivityHandler(); + handler.onTurn(turnContext).join(); + } + + @Test + public void TestTeamGetMembers() { + String baseUri = "https://test.coffee"; + MicrosoftAppCredentials credentials = MicrosoftAppCredentials.empty(); + ConnectorClient connectorClient = getConnectorClient(baseUri, credentials); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-Team-GetMembersAsync"); + activity.setChannelId(Channels.MSTEAMS); + TeamsChannelData data = new TeamsChannelData(); + data.setTeam(new TeamInfo("team-id")); + activity.setChannelData(data); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY, + getTeamsConnectorClient(connectorClient.baseUrl(), credentials) + ); + turnContext.getActivity().setServiceUrl("https://test.coffee"); + + ActivityHandler handler = new TestTeamsActivityHandler(); + handler.onTurn(turnContext).join(); + } + + @Test + public void TestGroupChatGetMembers() { + String baseUri = "https://test.coffee"; + MicrosoftAppCredentials credentials = MicrosoftAppCredentials.empty(); + ConnectorClient connectorClient = getConnectorClient(baseUri, credentials); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-GroupChat-GetMembersAsync"); + activity.setChannelId(Channels.MSTEAMS); + activity.setConversation(new ConversationAccount("conversation-id")); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY, + getTeamsConnectorClient(connectorClient.baseUrl(), credentials) + ); + turnContext.getActivity().setServiceUrl("https://test.coffee"); + + ActivityHandler handler = new TestTeamsActivityHandler(); + handler.onTurn(turnContext).join(); + } + + @Test + public void TestGetChannels() { + String baseUri = "https://test.coffee"; + MicrosoftAppCredentials credentials = MicrosoftAppCredentials.empty(); + ConnectorClient connectorClient = getConnectorClient(baseUri, credentials); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-GetChannelsAsync"); + activity.setChannelId(Channels.MSTEAMS); + TeamsChannelData data = new TeamsChannelData(); + data.setTeam(new TeamInfo("team-id")); + activity.setChannelData(data); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY, + getTeamsConnectorClient(connectorClient.baseUrl(), credentials) + ); + turnContext.getActivity().setServiceUrl("https://test.coffee"); + + ActivityHandler handler = new TestTeamsActivityHandler(); + handler.onTurn(turnContext).join(); + } + + @Test + public void TestGetMeetingInfo() { + String baseUri = "https://test.coffee"; + MicrosoftAppCredentials credentials = MicrosoftAppCredentials.empty(); + ConnectorClient connectorClient = getConnectorClient(baseUri, credentials); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-GetMeetingInfoAsync"); + activity.setChannelId(Channels.MSTEAMS); + TeamsChannelData data = new TeamsChannelData(); + data.setMeeting(new TeamsMeetingInfo("meeting-id")); + activity.setChannelData(data); + + TurnContext turnContext = new TurnContextImpl(new SimpleAdapter(), activity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add( + BotFrameworkAdapter.TEAMSCONNECTOR_CLIENT_KEY, + getTeamsConnectorClient(connectorClient.baseUrl(), credentials) + ); + turnContext.getActivity().setServiceUrl("https://test.coffee"); + + ActivityHandler handler = new TestTeamsActivityHandler(); + handler.onTurn(turnContext).join(); + } + + private class TestBotFrameworkAdapter extends BotFrameworkAdapter { + + public TestBotFrameworkAdapter(CredentialProvider withCredentialProvider) { + super(withCredentialProvider); + } + + @Override + protected CompletableFuture getOrCreateConnectorClient( + String serviceUrl, + AppCredentials usingAppCredentials + ) { + return CompletableFuture.completedFuture( + TeamsInfoTests.getConnectorClient(serviceUrl, usingAppCredentials) + ); + } + } + + private static class TestTeamsActivityHandler extends TeamsActivityHandler { + @Override + public CompletableFuture onTurn(TurnContext turnContext) { + return super.onTurn(turnContext).thenCompose(aVoid -> { + switch (turnContext.getActivity().getText()) { + case "Test-GetTeamDetailsAsync": + return callGetTeamDetails(turnContext); + + case "Test-Team-GetMembersAsync": + return callTeamGetMembers(turnContext); + + case "Test-GroupChat-GetMembersAsync": + return callGroupChatGetMembers(turnContext); + + case "Test-GetChannelsAsync": + return callGetChannels(turnContext); + + case "Test-SendMessageToTeamsChannelAsync": + return callSendMessageToTeamsChannel(turnContext); + + case "Test-GetMeetingInfoAsync": + return callTeamsInfoGetMeetingInfo(turnContext); + + default: + Assert.fail(); + } + + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally( + new AssertionError( + "Unknown Activity Text sent to TestTeamsActivityHandler.onTurn" + ) + ); + return result; + }); + } + + private CompletableFuture callSendMessageToTeamsChannel(TurnContext turnContext) { + Activity message = MessageFactory.text("hi"); + String channelId = "channelId123"; + MicrosoftAppCredentials creds = new MicrosoftAppCredentials( + "big-guid-here", + "appPasswordHere" + ); + Pair reference = TeamsInfo.sendMessageToTeamsChannel( + turnContext, + message, + channelId, + creds + ).join(); + + Assert.assertEquals("activityId123", reference.getLeft().getActivityId()); + Assert.assertEquals("channelId123", reference.getLeft().getChannelId()); + Assert.assertEquals("https://test.coffee", reference.getLeft().getServiceUrl()); + Assert.assertEquals("activityId123", reference.getRight()); + + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture callGetTeamDetails(TurnContext turnContext) { + TeamDetails teamDetails = TeamsInfo.getTeamDetails(turnContext, null).join(); + + Assert.assertEquals("team-id", teamDetails.getId()); + Assert.assertEquals("team-name", teamDetails.getName()); + Assert.assertEquals("team-aadgroupid", teamDetails.getAadGroupId()); + + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture callTeamGetMembers(TurnContext turnContext) { + List members = TeamsInfo.getMembers(turnContext).join(); + + Assert.assertEquals("id-1", members.get(0).getId()); + Assert.assertEquals("name-1", members.get(0).getName()); + Assert.assertEquals("givenName-1", members.get(0).getGivenName()); + Assert.assertEquals("surname-1", members.get(0).getSurname()); + Assert.assertEquals("userPrincipalName-1", members.get(0).getUserPrincipalName()); + + Assert.assertEquals("id-2", members.get(1).getId()); + Assert.assertEquals("name-2", members.get(1).getName()); + Assert.assertEquals("givenName-2", members.get(1).getGivenName()); + Assert.assertEquals("surname-2", members.get(1).getSurname()); + Assert.assertEquals("userPrincipalName-2", members.get(1).getUserPrincipalName()); + + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture callGroupChatGetMembers(TurnContext turnContext) { + List members = TeamsInfo.getMembers(turnContext).join(); + + Assert.assertEquals("id-3", members.get(0).getId()); + Assert.assertEquals("name-3", members.get(0).getName()); + Assert.assertEquals("givenName-3", members.get(0).getGivenName()); + Assert.assertEquals("surname-3", members.get(0).getSurname()); + Assert.assertEquals("userPrincipalName-3", members.get(0).getUserPrincipalName()); + + Assert.assertEquals("id-4", members.get(1).getId()); + Assert.assertEquals("name-4", members.get(1).getName()); + Assert.assertEquals("givenName-4", members.get(1).getGivenName()); + Assert.assertEquals("surname-4", members.get(1).getSurname()); + Assert.assertEquals("userPrincipalName-4", members.get(1).getUserPrincipalName()); + + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture callGetChannels(TurnContext turnContext) { + List channels = TeamsInfo.getTeamChannels(turnContext, null).join(); + + Assert.assertEquals("channel-id-1", channels.get(0).getId()); + + Assert.assertEquals("channel-id-2", channels.get(1).getId()); + Assert.assertEquals("channel-name-2", channels.get(1).getName()); + + Assert.assertEquals("channel-id-3", channels.get(2).getId()); + Assert.assertEquals("channel-name-3", channels.get(2).getName()); + + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture callTeamsInfoGetMeetingInfo(TurnContext turnContext) { + MeetingInfo meeting = TeamsInfo.getMeetingInfo(turnContext, null).join(); + + Assert.assertEquals("meeting-id", meeting.getDetails().getId()); + Assert.assertEquals("organizer-id", meeting.getOrganizer().getId()); + Assert.assertEquals("meetingConversationId-1", meeting.getConversation().getId()); + + return CompletableFuture.completedFuture(null); + } + } + + private static ConnectorClient getConnectorClient(String baseUri, AppCredentials credentials) { + Conversations mockConversations = Mockito.mock(Conversations.class); + + ConversationResourceResponse response = new ConversationResourceResponse(); + response.setId("team-id"); + response.setServiceUrl("https://serviceUrl/"); + response.setActivityId("activityId123"); + + // createConversation + Mockito.when( + mockConversations.createConversation(Mockito.any(ConversationParameters.class)) + ).thenReturn(CompletableFuture.completedFuture(response)); + + + ArrayList channelAccounts1 = new ArrayList(); + ChannelAccount channelAccount1 = new ChannelAccount(); + channelAccount1.setId("id-1"); + channelAccount1.setName("name-1"); + channelAccount1.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-1")); + channelAccount1.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-1")); + channelAccount1.setProperties("surname", JsonNodeFactory.instance.textNode("surname-1")); + channelAccount1.setProperties("email", JsonNodeFactory.instance.textNode("email-1")); + channelAccount1.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-1")); + channelAccount1.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-1")); + channelAccounts1.add(channelAccount1); + ChannelAccount channelAccount2 = new ChannelAccount(); + channelAccount2.setId("id-2"); + channelAccount2.setName("name-2"); + channelAccount2.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-2")); + channelAccount2.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-2")); + channelAccount2.setProperties("surname", JsonNodeFactory.instance.textNode("surname-2")); + channelAccount2.setProperties("email", JsonNodeFactory.instance.textNode("email-2")); + channelAccount2.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-2")); + channelAccount2.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-2")); + channelAccounts1.add(channelAccount2); + // getConversationMembers (Team) + Mockito.when(mockConversations.getConversationMembers("team-id")).thenReturn( + CompletableFuture.completedFuture(channelAccounts1) + ); + + + ArrayList channelAccounts2 = new ArrayList(); + ChannelAccount channelAccount3 = new ChannelAccount(); + channelAccount3.setId("id-3"); + channelAccount3.setName("name-3"); + channelAccount3.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-3")); + channelAccount3.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-3")); + channelAccount3.setProperties("surname", JsonNodeFactory.instance.textNode("surname-3")); + channelAccount3.setProperties("email", JsonNodeFactory.instance.textNode("email-3")); + channelAccount3.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-3")); + channelAccount3.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-3")); + channelAccounts2.add(channelAccount3); + ChannelAccount channelAccount4 = new ChannelAccount(); + channelAccount4.setId("id-4"); + channelAccount4.setName("name-4"); + channelAccount4.setProperties("objectId", JsonNodeFactory.instance.textNode("objectId-4")); + channelAccount4.setProperties("givenName", JsonNodeFactory.instance.textNode("givenName-4")); + channelAccount4.setProperties("surname", JsonNodeFactory.instance.textNode("surname-4")); + channelAccount4.setProperties("email", JsonNodeFactory.instance.textNode("email-4")); + channelAccount4.setProperties("userPrincipalName", JsonNodeFactory.instance.textNode("userPrincipalName-4")); + channelAccount4.setProperties("tenantId", JsonNodeFactory.instance.textNode("tenantId-4")); + channelAccounts2.add(channelAccount4); + // getConversationMembers (Group chat) + Mockito.when(mockConversations.getConversationMembers("conversation-id")).thenReturn( + CompletableFuture.completedFuture(channelAccounts2) + ); + + ConnectorClient mockConnectorClient = Mockito.mock(ConnectorClient.class); + Mockito.when(mockConnectorClient.getConversations()).thenReturn(mockConversations); + Mockito.when(mockConnectorClient.baseUrl()).thenReturn(baseUri); + Mockito.when(mockConnectorClient.credentials()).thenReturn(credentials); + + return mockConnectorClient; + } + + private static TeamsConnectorClient getTeamsConnectorClient( + String baseUri, + AppCredentials credentials + ) { + TeamsOperations mockOperations = Mockito.mock(TeamsOperations.class); + + + ConversationList list = new ConversationList(); + ArrayList conversations = new ArrayList(); + conversations.add(new ChannelInfo("channel-id-1")); + conversations.add(new ChannelInfo("channel-id-2", "channel-name-2")); + conversations.add(new ChannelInfo("channel-id-3", "channel-name-3")); + list.setConversations(conversations); + // fetchChannelList + Mockito.when(mockOperations.fetchChannelList(Mockito.anyString())).thenReturn( + CompletableFuture.completedFuture(list) + ); + + TeamDetails details = new TeamDetails(); + details.setId("team-id"); + details.setName("team-name"); + details.setAadGroupId("team-aadgroupid"); + // fetchTeamDetails + Mockito.when(mockOperations.fetchTeamDetails(Mockito.anyString())).thenReturn( + CompletableFuture.completedFuture(details) + ); + + // fetchTeamDetails + MeetingInfo meetingInfo = new MeetingInfo(); + MeetingDetails meetingDetails = new MeetingDetails(); + meetingDetails.setId("meeting-id"); + meetingInfo.setDetails(meetingDetails); + + TeamsChannelAccount organizer = new TeamsChannelAccount(); + organizer.setId("organizer-id"); + meetingInfo.setOrganizer(organizer); + + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId("meetingConversationId-1"); + meetingInfo.setConversation(conversationAccount); + + Mockito.when(mockOperations.fetchMeetingInfo(Mockito.anyString())).thenReturn( + CompletableFuture.completedFuture(meetingInfo) + ); + + TeamsConnectorClient mockConnectorClient = Mockito.mock(TeamsConnectorClient.class); + Mockito.when(mockConnectorClient.getTeams()).thenReturn(mockOperations); + Mockito.when(mockConnectorClient.baseUrl()).thenReturn(baseUri); + Mockito.when(mockConnectorClient.credentials()).thenReturn(credentials); + + return mockConnectorClient; + } +} diff --git a/libraries/bot-connector/pom.xml b/libraries/bot-connector/pom.xml index dd1241ba0..b533c369c 100644 --- a/libraries/bot-connector/pom.xml +++ b/libraries/bot-connector/pom.xml @@ -1,239 +1,201 @@ - 4.0.0 - - com.microsoft.bot.connector - bot-connector - jar - 4.0.0-SNAPSHOT - - - ${project.groupId}:${project.artifactId} - Bot Framework Connector - https://dev.botframework.com/ - - - - MIT License - http://www.opensource.org/licenses/mit-license.php - - - - - - Bot Framework Development - - Microsoft - https://dev.botframework.com/ - - - - - scm:git:https://github.com/Microsoft/botbuilder-java - scm:git:https://github.com/Microsoft/botbuilder-java - https://github.com/Microsoft/botbuilder-java - - - - UTF-8 - false - - - - - junit - junit - 4.12 - test - - - com.microsoft.rest - client-runtime - 1.2.1 - - - com.microsoft.azure - azure-client-runtime - 1.2.1 - - - com.microsoft.azure - azure-client-authentication - 1.2.1 - - - com.fasterxml.jackson.module - jackson-module-parameter-names - 2.9.2 - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - 2.9.2 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.2 - - - com.auth0 - java-jwt - 3.3.0 - - - com.auth0 - jwks-rsa - 0.3.0 - - - com.microsoft.bot.schema - botbuilder-schema - 4.0.0-SNAPSHOT - - - - - - - MyGet - https://botbuilder.myget.org/F/botbuilder-v4-java-daily/maven/ - - - - - - MyGet - https://botbuilder.myget.org/F/botbuilder-v4-java-daily/maven/ - - - - - - build - - true - - - - - src/main/resources - true - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.1 - - - - true - true - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.7.0 - - 1.8 - 1.8 - - - - - - - - publish - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.7.0 - - 1.8 - 1.8 - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.1 - - - - true - true - - - - - - - - - - + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-connector + jar + + ${project.groupId}:${project.artifactId} + Bot Framework Connector + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + org.mockito + mockito-core + + + org.skyscreamer + jsonassert + 1.5.0 + + + + + com.google.guava + guava + + + + com.squareup.retrofit2 + retrofit + 2.5.0 + + + com.squareup.retrofit2 + converter-jackson + 2.5.0 + + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + + + com.squareup.okhttp3 + okhttp-urlconnection + + + + com.microsoft.azure + azure-annotations + 1.7.0 + + + commons-codec + commons-codec + 1.15 + + + + com.microsoft.azure + msal4j + 1.11.0 + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.core + jackson-databind + + + com.auth0 + java-jwt + + + com.auth0 + jwks-rsa + + + commons-io + commons-io + + + + com.microsoft.bot + bot-schema + + + + + + build + + true + + + + + org.apache.maven.plugins + maven-pmd-plugin + + true + + com/microsoft/bot/azure/** + com/microsoft/bot/rest/** + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + com/microsoft/bot/restclient/** + + + + + + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${pmd.version} + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + com/microsoft/bot/restclient/** + + + + + checkstyle + + + + + + diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Async.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Async.java new file mode 100644 index 000000000..9ec3f5c0f --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Async.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import java.util.concurrent.CompletableFuture; + +/** + * Asyc and CompletableFuture helpers methods. + */ +public final class Async { + private Async() { + + } + + /** + * Executes a block and returns a CompletableFuture with either the return + * value or the exception (completeExceptionally). + * + * @param supplier The block to execute. + * @param The type of the CompletableFuture value. + * @return The CompletableFuture + */ + public static CompletableFuture wrapBlock(ThrowSupplier supplier) { + CompletableFuture result = new CompletableFuture<>(); + + try { + result.complete(supplier.get()); + } catch (Throwable t) { + result.completeExceptionally(t); + } + + return result; + } + + /** + * Executes a block that returns a CompletableFuture, and catches any exceptions in order + * to properly return a completed exceptionally result. + * + * @param supplier The block to execute. + * @param The type of the CompletableFuture value. + * @return The CompletableFuture + */ + public static CompletableFuture tryCompletable(ThrowSupplier> supplier) { + CompletableFuture result = new CompletableFuture<>(); + + try { + return supplier.get(); + } catch (Throwable t) { + result.completeExceptionally(t); + } + + return result; + } + + /** + * Constructs a CompletableFuture completed exceptionally. + * @param ex The exception. + * @param Type of CompletableFuture. + * @return A CompletableFuture with the exception. + */ + public static CompletableFuture completeExceptionally(Throwable ex) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(ex); + return result; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java index 7d1d86ecc..2bdc3915b 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Attachments.java @@ -2,116 +2,54 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is - * regenerated. */ package com.microsoft.bot.connector; -import com.microsoft.bot.schema.models.AttachmentInfo; -import com.microsoft.bot.connector.models.ErrorResponseException; -import com.microsoft.rest.ServiceCallback; -import com.microsoft.rest.ServiceFuture; -import com.microsoft.rest.ServiceResponse; +import com.microsoft.bot.schema.AttachmentInfo; + import java.io.InputStream; -import java.io.IOException; -import rx.Observable; +import java.util.concurrent.CompletableFuture; /** - * An instance of this class provides access to all the operations defined - * in Attachments. + * An instance of this class provides access to all the operations defined in + * Attachments. */ public interface Attachments { /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the AttachmentInfo object if successful. - */ - AttachmentInfo getAttachmentInfo(String attachmentId); - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture getAttachmentInfoAsync(String attachmentId, final ServiceCallback serviceCallback); - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the AttachmentInfo object - */ - Observable getAttachmentInfoAsync(String attachmentId); - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. + * GetAttachmentInfo. Get AttachmentInfo structure describing the attachment + * views. * * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the AttachmentInfo object - */ - Observable> getAttachmentInfoWithServiceResponseAsync(String attachmentId); - - /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the InputStream object if successful. */ - InputStream getAttachment(String attachmentId, String viewId); + CompletableFuture getAttachmentInfo(String attachmentId); /** - * GetAttachment. - * Get the named view as binary content. + * GetAttachment. Get the named view as binary content. * * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. + * @param viewId View id from attachmentInfo + * @return the observable to the InputStream object * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object */ - ServiceFuture getAttachmentAsync(String attachmentId, String viewId, final ServiceCallback serviceCallback); + CompletableFuture getAttachment(String attachmentId, String viewId); /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the InputStream object + * Get the URI of an attachment view. + * @param attachmentId id of the attachment. + * @param viewId default is "original". + * @return URI of the attachment. */ - Observable getAttachmentAsync(String attachmentId, String viewId); + String getAttachmentUri(String attachmentId, String viewId); /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the InputStream object + * Get the URI of an attachment view. + * @param attachmentId id of the attachment. + * @return URI of the attachment. */ - Observable> getAttachmentWithServiceResponseAsync(String attachmentId, String viewId); - + default String getAttachmentUri(String attachmentId) { + return getAttachmentUri(attachmentId, "original"); + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/BotSignIn.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/BotSignIn.java new file mode 100644 index 000000000..18b7689cd --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/BotSignIn.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.schema.SignInResource; + +/** + * An instance of this class provides access to all the operations defined in + * BotSignIns. + */ +public interface BotSignIn { + /** + * + * @param state the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + CompletableFuture getSignInUrl(String state); + + /** + * + * @param state the String value + * @param codeChallenge the String value + * @param emulatorUrl the String value + * @param finalRedirect the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + CompletableFuture getSignInUrl( + String state, + String codeChallenge, + String emulatorUrl, + String finalRedirect + ); + + /** + * + * @param state the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + CompletableFuture getSignInResource(String state); + /** + * + * @param state the String value + * @param codeChallenge the String value + * @param emulatorUrl the String value + * @param finalRedirect the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + CompletableFuture getSignInResource( + String state, + String codeChallenge, + String emulatorUrl, + String finalRedirect + ); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Channels.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Channels.java new file mode 100644 index 000000000..8a505f06a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Channels.java @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +/** + * Channel ID's. + */ +public final class Channels { + private Channels() { + + } + + /** + * Console channel. + */ + public static final String CONSOLE = "console"; + + /** + * Cortana channel. + */ + public static final String CORTANA = "cortana"; + + /** + * Direct Line channel. + */ + public static final String DIRECTLINE = "directline"; + + /** + * Direct Line Speech channel. + */ + public static final String DIRECTLINESPEECH = "directlinespeech"; + + /** + * Email channel. + */ + public static final String EMAIL = "email"; + + /** + * Emulator channel. + */ + public static final String EMULATOR = "emulator"; + + /** + * Facebook channel. + */ + public static final String FACEBOOK = "facebook"; + + /** + * Group Me channel. + */ + public static final String GROUPME = "groupme"; + + /** + * Kik channel. + */ + public static final String KIK = "kik"; + + /** + * Line channel. + */ + public static final String LINE = "line"; + + /** + * MS Teams channel. + */ + public static final String MSTEAMS = "msteams"; + + /** + * Skype channel. + */ + public static final String SKYPE = "skype"; + + /** + * Skype for Business channel. + */ + public static final String SKYPEFORBUSINESS = "skypeforbusiness"; + + /** + * Slack channel. + */ + public static final String SLACK = "slack"; + + /** + * SMS (Twilio) channel. + */ + public static final String SMS = "sms"; + + /** + * Telegram channel. + */ + public static final String TELEGRAM = "telegram"; + + /** + * WebChat channel. + */ + public static final String WEBCHAT = "webchat"; + + /** + * Test channel. + */ + public static final String TEST = "test"; + + /** + * Telephony channel. + */ + public static final String TELEPHONY = "telephony"; +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java index a1d509585..168439e3b 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClient.java @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * + *

* Code generated by Microsoft (R) AutoRest Code Generator. * Changes may cause incorrect behavior and will be lost if the code is * regenerated. @@ -10,88 +10,98 @@ package com.microsoft.bot.connector; -import com.microsoft.azure.AzureClient; -import com.microsoft.rest.RestClient; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; /** * The interface for ConnectorClient class. */ -public interface ConnectorClient { +public interface ConnectorClient extends AutoCloseable { /** * Gets the REST client. * * @return the {@link RestClient} object. - */ - RestClient restClient(); + */ + RestClient getRestClient(); /** - * Gets the {@link AzureClient} used for long running operations. - * @return the azure client; + * Returns the base url for this ConnectorClient. + * + * @return The base url. */ - AzureClient getAzureClient(); + String baseUrl(); + + /** + * Returns the credentials in use. + * + * @return The ServiceClientCredentials in use. + */ + ServiceClientCredentials credentials(); /** * Gets the User-Agent header for the client. * * @return the user agent string. */ - String userAgent(); + String getUserAgent(); /** - * Gets Gets or sets the preferred language for the response.. + * Gets the preferred language for the response.. * * @return the acceptLanguage value. */ - String acceptLanguage(); + String getAcceptLanguage(); /** - * Sets Gets or sets the preferred language for the response.. + * Sets the preferred language for the response.. * * @param acceptLanguage the acceptLanguage value. - * @return the service client itself */ - ConnectorClient withAcceptLanguage(String acceptLanguage); + void setAcceptLanguage(String acceptLanguage); /** - * Gets Gets or sets the retry timeout in seconds for Long Running Operations. Default value is 30.. + * Gets the retry timeout in seconds for Long Running Operations. Default value + * is 30.. * - * @return the longRunningOperationRetryTimeout value. + * @return the timeout value. */ - int longRunningOperationRetryTimeout(); + int getLongRunningOperationRetryTimeout(); /** - * Sets Gets or sets the retry timeout in seconds for Long Running Operations. Default value is 30.. + * Sets the retry timeout in seconds for Long Running Operations. Default value + * is 30. * - * @param longRunningOperationRetryTimeout the longRunningOperationRetryTimeout value. - * @return the service client itself + * @param timeout the longRunningOperationRetryTimeout value. */ - ConnectorClient withLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout); + void setLongRunningOperationRetryTimeout(int timeout); /** - * Gets When set to true a unique x-ms-client-request-id value is generated and included in each request. Default is true.. + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. is true. * * @return the generateClientRequestId value. */ - boolean generateClientRequestId(); + boolean getGenerateClientRequestId(); /** - * Sets When set to true a unique x-ms-client-request-id value is generated and included in each request. Default is true.. + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. Default is true. * * @param generateClientRequestId the generateClientRequestId value. - * @return the service client itself */ - ConnectorClient withGenerateClientRequestId(boolean generateClientRequestId); + void setGenerateClientRequestId(boolean generateClientRequestId); /** * Gets the Attachments object to access its operations. + * * @return the Attachments object. */ - Attachments attachments(); + Attachments getAttachments(); /** * Gets the Conversations object to access its operations. + * * @return the Conversations object. */ - Conversations conversations(); - + Conversations getConversations(); } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClientFuture.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClientFuture.java deleted file mode 100644 index cc5ab5194..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorClientFuture.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.microsoft.bot.connector; - -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.rest.RestClient; -import com.microsoft.rest.credentials.ServiceClientCredentials; - -public class ConnectorClientFuture extends ConnectorClientImpl { - - /** - * Initializes an instance of ConnectorClient client. - * - * @param credentials the management credentials for Azure - */ - public ConnectorClientFuture(ServiceClientCredentials credentials) { - super(credentials); - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param baseUrl the base URL of the host - * @param credentials the management credentials for Azure - */ - public ConnectorClientFuture(String baseUrl, ServiceClientCredentials credentials) { - super(baseUrl, credentials); - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param restClient the REST client to connect to Azure. - */ - public ConnectorClientFuture(RestClient restClient) { - super(restClient); - } - - @Override - public String userAgent() { - return "Microsoft-BotFramework/4.0"; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorConfiguration.java new file mode 100644 index 000000000..1c0dc0868 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConnectorConfiguration.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector; + +import java.io.InputStream; +import java.util.Properties; +import java.util.function.Consumer; + +/** + * Loads configuration properties for bot-connector. + * + * The version of the package will be in the project.properties file. + */ +public class ConnectorConfiguration { + /** + * Load and pass properties to a function. + * + * @param func The function to process the loaded properties. + */ + public void process(Consumer func) { + final Properties properties = new Properties(); + try (InputStream propStream = + UserAgent.class.getClassLoader().getResourceAsStream("project.properties")) { + + properties.load(propStream); + if (!properties.containsKey("version")) { + properties.setProperty("version", "4.0.0"); + } + func.accept(properties); + } catch (Throwable t) { + Properties p = new Properties(); + p.setProperty("version", "4.0.0"); + func.accept(p); + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConversationConstants.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConversationConstants.java new file mode 100644 index 000000000..d158a5799 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ConversationConstants.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +/** + * Values and constants used for Conversation specific info. + */ +public final class ConversationConstants { + private ConversationConstants() { + } + + /** + * The name of Http Request Header to add Conversation Id to skills requests. + */ + public static final String CONVERSATION_ID_HTTP_HEADERNAME = "x-ms-conversation-id"; +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java index 3660d1f4f..503710b8e 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Conversations.java @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. - * + *

* Code generated by Microsoft (R) AutoRest Code Generator. * Changes may cause incorrect behavior and will be lost if the code is * regenerated. @@ -10,678 +10,351 @@ package com.microsoft.bot.connector; -import com.microsoft.bot.schema.models.Activity; -import com.microsoft.bot.schema.models.AttachmentData; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.bot.schema.models.ConversationParameters; -import com.microsoft.bot.schema.models.ConversationResourceResponse; -import com.microsoft.bot.schema.models.ConversationsResult; -import com.microsoft.bot.connector.models.ErrorResponseException; -import com.microsoft.bot.schema.models.ResourceResponse; -import com.microsoft.rest.ServiceCallback; -import com.microsoft.rest.ServiceFuture; -import com.microsoft.rest.ServiceResponse; -import java.io.IOException; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.AttachmentData; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.PagedMembersResult; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Transcript; +import org.apache.commons.lang3.StringUtils; + import java.util.List; -import rx.Observable; +import java.util.concurrent.CompletableFuture; /** - * An instance of this class provides access to all the operations defined - * in Conversations. + * An instance of this class provides access to all the operations defined in + * Conversations. */ public interface Conversations { /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ConversationsResult object if successful. - */ - ConversationsResult getConversations(); - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture getConversationsAsync(final ServiceCallback serviceCallback); - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationsResult object - */ - Observable getConversationsAsync(); - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationsResult object - */ - Observable> getConversationsWithServiceResponseAsync(); - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param continuationToken skip or continuation token - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ConversationsResult object if successful. - */ - ConversationsResult getConversations(String continuationToken); - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. + * GetConversations. List the Conversations in which this bot has participated. + * GET from this method with a skip token The return value is a + * ConversationsResult, which contains an array of ConversationMembers and a + * skip token. If the skip token is not empty, then there are further values to + * be returned. Call this method again with the returned token to get more + * values. * - * @param continuationToken skip or continuation token - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture getConversationsAsync(String continuationToken, final ServiceCallback serviceCallback); - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. + * Each ConversationMembers object contains the ID of the conversation and an + * array of ChannelAccounts that describe the members of the conversation. * - * @param continuationToken skip or continuation token * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ConversationsResult object */ - Observable getConversationsAsync(String continuationToken); + CompletableFuture getConversations(); /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. + * GetConversations. List the Conversations in which this bot has participated. + * GET from this method with a skip token The return value is a + * ConversationsResult, which contains an array of ConversationMembers and a + * skip token. If the skip token is not empty, then there are further values to + * be returned. Call this method again with the returned token to get more + * values. Each ConversationMembers object contains the ID of the conversation + * and an array of ChannelAccounts that describe the members of the + * conversation. * * @param continuationToken skip or continuation token * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ConversationsResult object */ - Observable> getConversationsWithServiceResponseAsync(String continuationToken); - - /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ConversationResourceResponse object if successful. - */ - ConversationResourceResponse createConversation(ConversationParameters parameters); - - /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture createConversationAsync(ConversationParameters parameters, final ServiceCallback serviceCallback); + CompletableFuture getConversations(String continuationToken); /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. + * CreateConversation. Create a new Conversation. POST to this method with a Bot + * being the bot creating the conversation IsGroup set to true if this is not a + * direct message (default is false) Members array contining the members you + * want to have be in the conversation. The return value is a ResourceResponse + * which contains a conversation id which is suitable for use in the message + * payload and REST API uris. Most channels only support the semantics of bots + * initiating a direct message conversation. An example of how to do that would + * be: ``` var resource = await connector.conversations.CreateConversation(new + * ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new + * ChannelAccount("user1") } ); await + * connect.Conversations.SendToConversation(resource.Id, new Activity() ... ) ; + * ``` * * @param parameters Parameters to create the conversation from * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ConversationResourceResponse object */ - Observable createConversationAsync(ConversationParameters parameters); + CompletableFuture createConversation( + ConversationParameters parameters + ); /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationResourceResponse object - */ - Observable> createConversationWithServiceResponseAsync(ConversationParameters parameters); - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. + * SendToConversation. This method allows you to send an activity to the end of + * a conversation. This is slightly different from ReplyToActivity(). + * SendToConverstion(conversationId) - will append the activity to the end of + * the conversation according to the timestamp or semantics of the channel. + * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to + * another activity, if the channel supports it. If the channel does not support + * nested replies, ReplyToActivity falls back to SendToConversation. Use + * ReplyToActivity when replying to a specific activity in the conversation. Use + * SendToConversation in all other cases. * * @param conversationId Conversation ID - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - ResourceResponse sendToConversation(String conversationId, Activity activity); - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activity Activity to send - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture sendToConversationAsync(String conversationId, Activity activity, final ServiceCallback serviceCallback); - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activity Activity to send + * @param activity Activity to send * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ResourceResponse object */ - Observable sendToConversationAsync(String conversationId, Activity activity); + CompletableFuture sendToConversation( + String conversationId, + Activity activity + ); /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. + * SendToConversation. This method allows you to send an activity to the end of + * a conversation. This is slightly different from ReplyToActivity(). + * sendToConverstion(activity) - will append the activity to the end of the + * conversation according to the timestamp or semantics of the channel, using + * the Activity.getConversation.getId for the conversation id. + * replyToActivity(conversationId,ActivityId) - adds the activity as a reply to + * another activity, if the channel supports it. If the channel does not support + * nested replies, ReplyToActivity falls back to SendToConversation. Use + * ReplyToActivity when replying to a specific activity in the conversation. Use + * SendToConversation in all other cases. * - * @param conversationId Conversation ID * @param activity Activity to send * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ResourceResponse object */ - Observable> sendToConversationWithServiceResponseAsync(String conversationId, Activity activity); - - /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. - * - * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - ResourceResponse updateActivity(String conversationId, String activityId, Activity activity); - - /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. - * - * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture updateActivityAsync(String conversationId, String activityId, Activity activity, final ServiceCallback serviceCallback); + default CompletableFuture sendToConversation(Activity activity) { + return sendToConversation(activity.getConversation().getId(), activity); + } /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. + * UpdateActivity. Edit an existing activity. Some channels allow you to edit an + * existing activity to reflect the new state of a bot conversation. For + * example, you can remove buttons after someone has clicked "Approve" button. * * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity + * @param activityId activityId to update + * @param activity replacement Activity * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ResourceResponse object */ - Observable updateActivityAsync(String conversationId, String activityId, Activity activity); + CompletableFuture updateActivity( + String conversationId, + String activityId, + Activity activity + ); /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. + * UpdateActivity. Edit an existing activity. Some channels allow you to edit an + * existing activity to reflect the new state of a bot conversation. For + * example, you can remove buttons after someone has clicked "Approve" button. * - * @param conversationId Conversation ID - * @param activityId activityId to update * @param activity replacement Activity * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ResourceResponse object */ - Observable> updateActivityWithServiceResponseAsync(String conversationId, String activityId, Activity activity); + default CompletableFuture updateActivity(Activity activity) { + return updateActivity(activity.getConversation().getId(), activity.getId(), activity); + } /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. + * ReplyToActivity. This method allows you to reply to an activity. This is + * slightly different from SendToConversation(). + * SendToConversation(conversationId) - will append the activity to the end of + * the conversation according to the timestamp or semantics of the channel. + * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to + * another activity, if the channel supports it. If the channel does not support + * nested replies, ReplyToActivity falls back to SendToConversation. Use + * ReplyToActivity when replying to a specific activity in the conversation. Use + * SendToConversation in all other cases. * * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - ResourceResponse replyToActivity(String conversationId, String activityId, Activity activity); - - /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture replyToActivityAsync(String conversationId, String activityId, Activity activity, final ServiceCallback serviceCallback); - - /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send + * @param activityId activityId the reply is to (OPTIONAL) + * @param activity Activity to send * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ResourceResponse object */ - Observable replyToActivityAsync(String conversationId, String activityId, Activity activity); + CompletableFuture replyToActivity( + String conversationId, + String activityId, + Activity activity + ); /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. + * ReplyToActivity. This method allows you to reply to an activity. This is + * slightly different from SendToConversation(). + * SendToConversation(conversationId) - will append the activity to the end of + * the conversation according to the timestamp or semantics of the channel. + * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to + * another activity, if the channel supports it. If the channel does not support + * nested replies, ReplyToActivity falls back to SendToConversation. Use + * ReplyToActivity when replying to a specific activity in the conversation. Use + * SendToConversation in all other cases. * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) * @param activity Activity to send * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the ResourceResponse object */ - Observable> replyToActivityWithServiceResponseAsync(String conversationId, String activityId, Activity activity); + default CompletableFuture replyToActivity(Activity activity) { + if (StringUtils.isEmpty(activity.getReplyToId())) { + return Async.completeExceptionally(new IllegalArgumentException( + "ReplyToId cannot be empty" + )); + } - /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. - * - * @param conversationId Conversation ID - * @param activityId activityId to delete - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - */ - void deleteActivity(String conversationId, String activityId); + return replyToActivity( + activity.getConversation().getId(), activity.getReplyToId(), activity + ); + } /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. + * DeleteActivity. Delete an existing activity. Some channels allow you to + * delete an existing activity, and if successful this method will remove the + * specified activity. * * @param conversationId Conversation ID - * @param activityId activityId to delete - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. + * @param activityId activityId to delete * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object + * @return the {@link com.microsoft.bot.restclient.ServiceResponse} object if + * successful. */ - ServiceFuture deleteActivityAsync(String conversationId, String activityId, final ServiceCallback serviceCallback); + CompletableFuture deleteActivity(String conversationId, String activityId); /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. + * GetConversationMembers. Enumerate the members of a converstion. This REST API + * takes a ConversationId and returns an array of ChannelAccount objects + * representing the members of the conversation. * * @param conversationId Conversation ID - * @param activityId activityId to delete * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - Observable deleteActivityAsync(String conversationId, String activityId); - - /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. - * - * @param conversationId Conversation ID - * @param activityId activityId to delete - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - Observable> deleteActivityWithServiceResponseAsync(String conversationId, String activityId); - - /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. - * - * @param conversationId Conversation ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the List<ChannelAccount> object if successful. + * @return the observable to the List<ChannelAccount> object */ - List getConversationMembers(String conversationId); + CompletableFuture> getConversationMembers(String conversationId); /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. + * Retrieves a single member of a conversation by ID. * - * @param conversationId Conversation ID - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object + * @param userId The user id. + * @param conversationId The conversation id. + * @return The ChannelAccount for the user. */ - ServiceFuture> getConversationMembersAsync(String conversationId, final ServiceCallback> serviceCallback); + CompletableFuture getConversationMember(String userId, String conversationId); /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. + * DeleteConversationMember. Deletes a member from a conversation. This REST API + * takes a ConversationId and a memberId (of type string) and removes that + * member from the conversation. If that member was the last member of the + * conversation, the conversation will also be deleted. * * @param conversationId Conversation ID + * @param memberId ID of the member to delete from this conversation * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object + * @return the {@link com.microsoft.bot.restclient.ServiceResponse} object if + * successful. */ - Observable> getConversationMembersAsync(String conversationId); + CompletableFuture deleteConversationMember(String conversationId, String memberId); /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. + * GetActivityMembers. Enumerate the members of an activity. This REST API takes + * a ConversationId and a ActivityId, returning an array of ChannelAccount + * objects representing the members of the particular activity in the + * conversation. * * @param conversationId Conversation ID + * @param activityId Activity ID * @throws IllegalArgumentException thrown if parameters fail the validation * @return the observable to the List<ChannelAccount> object */ - Observable>> getConversationMembersWithServiceResponseAsync(String conversationId); + CompletableFuture> getActivityMembers( + String conversationId, + String activityId + ); /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. + * UploadAttachment. Upload an attachment directly into a channel's blob + * storage. This is useful because it allows you to store data in a compliant + * store when dealing with enterprises. The response is a ResourceResponse which + * contains an AttachmentId which is suitable for using with the attachments + * API. * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - */ - void deleteConversationMember(String conversationId, String memberId); - - /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. - * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. + * @param conversationId Conversation ID + * @param attachmentUpload Attachment data * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object + * @return the observable to the ResourceResponse object */ - ServiceFuture deleteConversationMemberAsync(String conversationId, String memberId, final ServiceCallback serviceCallback); + CompletableFuture uploadAttachment( + String conversationId, + AttachmentData attachmentUpload + ); /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. + * This method allows you to upload the historic activities to the conversation. * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - Observable deleteConversationMemberAsync(String conversationId, String memberId); - - /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. + * Sender must ensure that the historic activities have unique ids and + * appropriate timestamps. The ids are used by the client to deal with duplicate + * activities and the timestamps are used by the client to render the activities + * in the right order. * * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation + * @param history Historic activities * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. + * @throws RuntimeException all other wrapped checked exceptions if the + * request fails to be sent + * @return the ResourceResponse object if successful. */ - Observable> deleteConversationMemberWithServiceResponseAsync(String conversationId, String memberId); + CompletableFuture sendConversationHistory( + String conversationId, + Transcript history + ); /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. + * Enumerate the members of a conversation one page at a time. * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the List<ChannelAccount> object if successful. - */ - List getActivityMembers(String conversationId, String activityId); - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. + * This REST API takes a ConversationId. Optionally a pageSize and/or + * continuationToken can be provided. It returns a PagedMembersResult, which + * contains an array of ChannelAccounts representing the members of the + * conversation and a continuation token that can be used to get more values. * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture> getActivityMembersAsync(String conversationId, String activityId, final ServiceCallback> serviceCallback); - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. + * One page of ChannelAccounts records are returned with each call. The number + * of records in a page may vary between channels and calls. If there are no + * additional results the response will not contain a continuation token. If + * there are no members in the conversation the Members will be empty or not + * present in the response. * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object - */ - Observable> getActivityMembersAsync(String conversationId, String activityId); - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. + * A response to a request that has a continuation token from a prior request + * may rarely return members from a previous request. * * @param conversationId Conversation ID - * @param activityId Activity ID * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object + * @throws RuntimeException all other wrapped checked exceptions if the + * request fails to be sent + * @return the PagedMembersResult object if successful. */ - Observable>> getActivityMembersWithServiceResponseAsync(String conversationId, String activityId); + CompletableFuture getConversationPagedMembers(String conversationId); /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. + * Enumerate the members of a conversation one page at a time. * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - ResourceResponse uploadAttachment(String conversationId, AttachmentData attachmentUpload); - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. + * This REST API takes a ConversationId. Optionally a pageSize and/or + * continuationToken can be provided. It returns a PagedMembersResult, which + * contains an array of ChannelAccounts representing the members of the + * conversation and a continuation token that can be used to get more values. * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - ServiceFuture uploadAttachmentAsync(String conversationId, AttachmentData attachmentUpload, final ServiceCallback serviceCallback); - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. + * One page of ChannelAccounts records are returned with each call. The number + * of records in a page may vary between channels and calls. If there are no + * additional results the response will not contain a continuation token. If + * there are no members in the conversation the Members will be empty or not + * present in the response. * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - Observable uploadAttachmentAsync(String conversationId, AttachmentData attachmentUpload); - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. + * A response to a request that has a continuation token from a prior request + * may rarely return members from a previous request. * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data + * @param conversationId Conversation ID + * @param continuationToken The continuationToken from a previous call. * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object + * @throws RuntimeException all other wrapped checked exceptions if the + * request fails to be sent + * @return the PagedMembersResult object if successful. */ - Observable> uploadAttachmentWithServiceResponseAsync(String conversationId, AttachmentData attachmentUpload); - + CompletableFuture getConversationPagedMembers( + String conversationId, + String continuationToken + ); } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ExecutorFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ExecutorFactory.java new file mode 100644 index 000000000..6b4c507d0 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ExecutorFactory.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; +import java.util.concurrent.ForkJoinWorkerThread; + +/** + * Provides a common Executor for Future operations. + */ +public final class ExecutorFactory { + private ExecutorFactory() { + + } + + private static ForkJoinWorkerThreadFactory factory = new ForkJoinWorkerThreadFactory() { + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread worker = + ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + worker.setName("Bot-" + worker.getPoolIndex()); + return worker; + } + }; + + private static ExecutorService executor = + new ForkJoinPool(Runtime.getRuntime().availableProcessors() * 2, factory, null, false); + + /** + * Provides an SDK wide ExecutorService for async calls. + * + * @return An ExecutorService. + */ + public static ExecutorService getExecutor() { + return executor; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/OAuthClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/OAuthClient.java new file mode 100644 index 000000000..9a9b6cce2 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/OAuthClient.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +/** + * OAuth client interface. + */ +public interface OAuthClient { + /** + * Gets the BotSignIns object to access its operations. + * + * @return the BotSignIns object. + */ + BotSignIn getBotSignIn(); + + /** + * Gets the UserTokens object to access its operations. + * + * @return the UserTokens object. + */ + UserToken getUserToken(); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/OAuthClientConfig.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/OAuthClientConfig.java new file mode 100644 index 000000000..e6dfc0d5e --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/OAuthClientConfig.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.AuthenticationConstants; + +/** + * OAuthClient config. + */ +public final class OAuthClientConfig { + private OAuthClientConfig() { + + } + + /** + * The default endpoint that is used for API requests. + */ + public static final String OAUTHENDPOINT = AuthenticationConstants.OAUTH_URL; + + /** + * Value indicating whether when using the Emulator, whether to emulate the + * OAuthCard behavior or use connected flows. + */ + @SuppressWarnings("checkstyle:VisibilityModifier") + public static boolean emulateOAuthCards = false; +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ThrowSupplier.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ThrowSupplier.java new file mode 100644 index 000000000..9e8141f44 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/ThrowSupplier.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +/** + * A Supplier that throws. + * @param The type of the Supplier return value. + */ +@FunctionalInterface +public interface ThrowSupplier { + /** + * Gets a result. + * + * @return a result + * @throws Throwable Any exception + */ + T get() throws Throwable; +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java index 84febd4db..fb5d66bd8 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserAgent.java @@ -1,42 +1,38 @@ -package com.microsoft.bot.connector; +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; +package com.microsoft.bot.connector; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; +import org.slf4j.LoggerFactory; /** - * Retrieve the User Agent string that BotBuilder uses + * Retrieve the User Agent string that Bot SDK uses. *

* Conforms to spec: * https://github.com/Microsoft/botbuilder-dotnet/blob/d342cd66d159a023ac435aec0fdf791f93118f5f/doc/UserAgents.md - *

+ * */ -public class UserAgent { - - +public final class UserAgent { // os/java and botbuilder will never change - static initialize once - private static String os_java_botbuilder_cache; + private static String osJavaBotbuilderCache; static { - String build_version; - final Properties properties = new Properties(); - try { - InputStream propStream = ConnectorClientImpl.class.getClassLoader().getResourceAsStream("project.properties"); - properties.load(propStream); - build_version = properties.getProperty("version"); - } catch (IOException e) { - e.printStackTrace(); - build_version = "4.0.0"; - } - String os_version = System.getProperty("os.name"); - String java_version = System.getProperty("java.version"); - os_java_botbuilder_cache = String.format("BotBuilder/%s (JVM %s; %s)", build_version, java_version, os_version); + new ConnectorConfiguration().process(properties -> { + String buildVersion = properties.getProperty("version"); + String osVersion = System.getProperty("os.name"); + String javaVersion = System.getProperty("java.version"); + osJavaBotbuilderCache = + String.format("BotBuilder/%s (JVM %s; %s)", buildVersion, javaVersion, osVersion); + + LoggerFactory.getLogger(UserAgent.class).info("UserAgent: {}", osJavaBotbuilderCache); + }); } /** - * Private Constructor - Static Object + * Private Constructor - Static Object. */ private UserAgent() { @@ -44,9 +40,10 @@ private UserAgent() { /** * Retrieve the user agent string for BotBuilder. + * + * @return THe user agent string. */ public static String value() { - return os_java_botbuilder_cache; + return osJavaBotbuilderCache; } - } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserToken.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserToken.java new file mode 100644 index 000000000..4b9686984 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/UserToken.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector; + +import com.microsoft.bot.schema.AadResourceUrls; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * An instance of this class provides access to all the operations defined in + * UserTokens. + */ +public interface UserToken { + /** + * + * @param userId the String value + * @param connectionName the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the TokenResponse object + */ + CompletableFuture getToken(String userId, String connectionName); + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param channelId the String value + * @param code the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the TokenResponse object + */ + CompletableFuture getToken( + String userId, + String connectionName, + String channelId, + String code + ); + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param channelId the String value + * @param exchangeRequest a TokenExchangeRequest + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the TokenResponse object + */ + CompletableFuture exchangeToken( + String userId, + String connectionName, + String channelId, + TokenExchangeRequest exchangeRequest + ); + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param aadResourceUrls the AadResourceUrls value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Map<String, TokenResponse> object + */ + CompletableFuture> getAadTokens( + String userId, + String connectionName, + AadResourceUrls aadResourceUrls + ); + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param aadResourceUrls the AadResourceUrls value + * @param channelId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Map<String, TokenResponse> object + */ + CompletableFuture> getAadTokens( + String userId, + String connectionName, + AadResourceUrls aadResourceUrls, + String channelId + ); + + /** + * + * @param userId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Object object + */ + CompletableFuture signOut(String userId); + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param channelId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Object object + */ + CompletableFuture signOut(String userId, String connectionName, String channelId); + + /** + * + * @param userId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the List<TokenStatus> object + */ + CompletableFuture> getTokenStatus(String userId); + + /** + * + * @param userId the String value + * @param channelId the String value + * @param include the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the List<TokenStatus> object + */ + CompletableFuture> getTokenStatus( + String userId, + String channelId, + String include + ); + + /** + * Send a dummy OAuth card when the bot is being used on the Emulator for testing without fetching a real token. + * + * @param emulateOAuthCards Indicates whether the Emulator should emulate the OAuth card. + * @return A task that represents the work queued to execute. + */ + CompletableFuture sendEmulateOAuthCards(boolean emulateOAuthCards); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AllowedCallersClaimsValidator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AllowedCallersClaimsValidator.java new file mode 100644 index 000000000..71cc7bd77 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AllowedCallersClaimsValidator.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.Async; + +/** + * Sample claims validator that loads an allowed list from configuration if + * presentand checks that requests are coming from allowed parent bots. + */ +public class AllowedCallersClaimsValidator extends ClaimsValidator { + + private List allowedCallers; + + /** + * Creates an instance of an {@link AllowedCallersClaimsValidator}. + * @param withAllowedCallers A {@link List} that contains the list of allowed callers. + */ + public AllowedCallersClaimsValidator(List withAllowedCallers) { + this.allowedCallers = withAllowedCallers != null ? withAllowedCallers : new ArrayList(); + } + + /** + * Validates a Map of claims and should throw an exception if the + * validation fails. + * + * @param claims The Map of claims to validate. + * + * @return true if the validation is successful, false if not. + */ + @Override + public CompletableFuture validateClaims(Map claims) { + if (claims == null) { + return Async.completeExceptionally(new IllegalArgumentException("Claims cannot be null")); + } + + // If _allowedCallers contains an "*", we allow all callers. + if (SkillValidation.isSkillClaim(claims) && !allowedCallers.contains("*")) { + // Check that the appId claim in the skill request instanceof in the list of + // callers configured for this bot. + String appId = JwtTokenValidation.getAppIdFromClaims(claims); + if (!allowedCallers.contains(appId)) { + return Async.completeExceptionally( + new RuntimeException( + String.format( + "Received a request from a bot with an app ID of \"%s\". To enable requests from this " + + "caller, add the app ID to the configured set of allowedCallers.", + appId + ) + ) + ); + } + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java new file mode 100644 index 000000000..5be313ce4 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +/** + * Base abstraction for AAD credentials for auth and caching. + * + *

+ * Subclasses must provide the impl for {@link #buildAuthenticator} + *

+ */ +public abstract class AppCredentials implements ServiceClientCredentials { + + private String appId; + private String authTenant; + private String authScope; + private Authenticator authenticator; + + /** + * Initializes a new instance of the AppCredentials class. + * + * @param withChannelAuthTenant Optional. The oauth token tenant. + */ + public AppCredentials(String withChannelAuthTenant) { + this(withChannelAuthTenant, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + } + + /** + * Initializes a new instance of the AppCredentials class. + * + * @param withChannelAuthTenant Optional. The oauth token tenant. + * @param withOAuthScope The scope for the token. + */ + public AppCredentials(String withChannelAuthTenant, String withOAuthScope) { + setChannelAuthTenant(withChannelAuthTenant); + authScope = StringUtils.isEmpty(withOAuthScope) + ? AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + : withOAuthScope; + } + + /** + * Gets the App ID for this credential. + * + * @return The app id. + */ + public String getAppId() { + return appId; + } + + /** + * Sets the Microsoft app ID for this credential. + * + * @param withAppId The app id. + */ + public void setAppId(String withAppId) { + appId = withAppId; + } + + /** + * Gets tenant to be used for channel authentication. + * + * @return Tenant to be used for channel authentication. + */ + public String getChannelAuthTenant() { + return StringUtils.isEmpty(authTenant) + ? AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT + : getAuthTenant(); + } + + /** + * Sets tenant to be used for channel authentication. + * + * @param withAuthTenant Tenant to be used for channel authentication. + */ + public void setChannelAuthTenant(String withAuthTenant) { + try { + // Advanced user only, see https://aka.ms/bots/tenant-restriction + String endPointUrl = String.format( + AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, withAuthTenant + ); + new URL(endPointUrl).toString(); + setAuthTenant(withAuthTenant); + } catch (MalformedURLException e) { + throw new AuthenticationException("Invalid channel auth tenant: " + withAuthTenant); + } + } + + /** + * OAuth endpoint to use. + * + * @return The OAuth endpoint. + */ + public String oAuthEndpoint() { + return String.format( + AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, getChannelAuthTenant() + ); + } + + /** + * OAuth scope to use. + * + * @return OAuth scope. + */ + public String oAuthScope() { + return authScope; + } + + /** + * Gets the channel auth token tenant for this credential. + * + * @return The channel auth token tenant. + */ + protected String getAuthTenant() { + return authTenant; + } + + /** + * Sets the channel auth token tenant for this credential. + * + * @param withAuthTenant The auth token tenant. + */ + protected void setAuthTenant(String withAuthTenant) { + authTenant = withAuthTenant; + } + + /** + * Gets an OAuth access token. + * + * @return If the task is successful, the result contains the access token + * string. + */ + public CompletableFuture getToken() { + CompletableFuture result; + + try { + result = getAuthenticator().acquireToken() + .thenApply(IAuthenticationResult::accessToken); + } catch (MalformedURLException e) { + result = new CompletableFuture<>(); + result.completeExceptionally(new AuthenticationException(e)); + } + + return result; + } + + /** + * Called by the {@link AppCredentialsInterceptor} to determine if the HTTP + * request should be modified to contain the token. + * + * @param url The HTTP request URL. + * @return true if the auth token should be added to the request. + */ + boolean shouldSetToken(String url) { + if (StringUtils.isBlank(getAppId()) || getAppId().equals(AuthenticationConstants.ANONYMOUS_SKILL_APPID)) { + return false; + } + return true; + } + + // lazy Authenticator create. + private Authenticator getAuthenticator() throws MalformedURLException { + if (authenticator == null) { + authenticator = buildAuthenticator(); + } + return authenticator; + } + + /** + * Returns an appropriate Authenticator that is provided by a subclass. + * + * @return An Authenticator object. + * @throws MalformedURLException If the endpoint isn't valid. + */ + protected abstract Authenticator buildAuthenticator() throws MalformedURLException; + + /** + * Apply the credentials to the HTTP request. + * + *

+ * Note: Provides the same functionality as dotnet ProcessHttpRequestAsync + *

+ * + * @param clientBuilder the builder for building up an {@link OkHttpClient} + */ + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new AppCredentialsInterceptor(this)); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentialsInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentialsInterceptor.java new file mode 100644 index 000000000..cdb5baf6e --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentialsInterceptor.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * Token credentials filter for placing a token credential into request headers. + */ +public class AppCredentialsInterceptor implements Interceptor { + /** + * The credentials instance to apply to the HTTP client pipeline. + */ + private AppCredentials credentials; + + /** + * Initialize a TokenCredentialsFilter class with a TokenCredentials credential. + * + * @param withCredentials a TokenCredentials instance + */ + public AppCredentialsInterceptor(AppCredentials withCredentials) { + credentials = withCredentials; + } + + /** + * Apply the credentials to the HTTP request. + * + * @param chain The Okhttp3 Interceptor Chain. + * @return The modified Response. + * @throws IOException via Chain or failure to get token. + */ + @Override + public Response intercept(Chain chain) throws IOException { + if (credentials.shouldSetToken(chain.request().url().url().toString())) { + String token; + try { + token = credentials.getToken().get(); + } catch (Throwable t) { + throw new IOException(t); + } + + Request newRequest = + chain.request().newBuilder().header("Authorization", "Bearer " + token).build(); + return chain.proceed(newRequest); + } + return chain.proceed(chain.request()); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java new file mode 100644 index 000000000..2754253ea --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.ArrayList; +import java.util.List; + +/** + * General configuration settings for authentication. + */ +public class AuthenticationConfiguration { + + private ClaimsValidator claimsValidator = null; + + /** + * Required endorsements for auth. + * + * @return A List of endorsements. + */ + public List requiredEndorsements() { + return new ArrayList(); + } + + /** + * Access to the ClaimsValidator used to validate the identity claims. + * @return the ClaimsValidator value if set. + */ + public ClaimsValidator getClaimsValidator() { + return claimsValidator; + } + + /** + * Access to the ClaimsValidator used to validate the identity claims. + * @param withClaimsValidator the value to set the ClaimsValidator to. + */ + public void setClaimsValidator(ClaimsValidator withClaimsValidator) { + claimsValidator = withClaimsValidator; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java index b8769b30c..3d9cbfb9b 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java @@ -6,28 +6,159 @@ import java.util.ArrayList; import java.util.List; +/** + * Values and Constants used for Authentication and Authorization by the Bot + * Framework Protocol. + */ public final class AuthenticationConstants { - public static final String ToChannelFromBotLoginUrl = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"; - public static final String ToChannelFromBotOAuthScope = "https://api.botframework.com/.default"; - public static final String ToBotFromChannelTokenIssuer = "https://api.botframework.com"; - public static final String ToBotFromChannelOpenIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openidconfiguration"; - public static final String ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; - public static final List AllowedSigningAlgorithms = new ArrayList<>(); - public static final String AuthorizedParty = "azp"; - public static final String AudienceClaim = "aud"; - public static final String ServiceUrlClaim = "serviceurl"; - public static final String VersionClaim = "ver"; - public static final String AppIdClaim = "appid"; + private AuthenticationConstants() { + + } + + /** + * TO CHANNEL FROM BOT: Login URL. + */ + @Deprecated + public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL = + "https://login.microsoftonline.com/botframework.com"; + + /** + * TO CHANNEL FROM BOT: Login URL template string. Bot developer may specify + * which tenant to obtain an access token from. By default, the channels only + * accept tokens from "botframework.com". For more details see + * https://aka.ms/bots/tenant-restriction. + */ + public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE = + "https://login.microsoftonline.com/%s"; + + /** + * TO CHANNEL FROM BOT: OAuth scope to request. + */ + public static final String TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = + "https://api.botframework.com/.default"; + + /** + * TO BOT FROM CHANNEL: Token issuer. + */ + public static final String TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com"; + + /** + * TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = + "https://login.botframework.com/v1/.well-known/openidconfiguration"; + + /** + * TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA. + */ + public static final String TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = + "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + + /** + * TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming + * from MSA. + */ + public static final String TO_BOT_FROM_ENTERPRISE_CHANNEL_OPENID_METADATA_URL_FORMAT = + "https://%s.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration"; + + /** + * Allowed token signing algorithms. Tokens come from channels to the bot. The + * code that uses this also supports tokens coming from the emulator. + */ + public static final List ALLOWED_SIGNING_ALGORITHMS = new ArrayList<>(); + + /** + * Application Setting Key for the OAuthUrl value. + */ + public static final String OAUTH_URL_KEY = "OAuthApiEndpoint"; + + /** + * OAuth Url used to get a token from OAuthApiClient. + */ + public static final String OAUTH_URL = "https://api.botframework.com"; + + /** + * Application Settings Key for whether to emulate OAuthCards when using the + * emulator. + */ + public static final String EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards"; + + /** + * Application Setting Key for the OpenIdMetadataUrl value. + */ + public static final String BOT_OPENID_METADATA_KEY = "BotOpenIdMetadata"; + + /** + * The default tenant to acquire bot to channel token from. + */ + public static final String DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com"; + + /** + * "azp" Claim. Authorized party - the party to which the ID Token was issued. + * This claim follows the general format set forth in the OpenID Spec. + * http://openid.net/specs/openid-connect-core-1_0.html#IDToken. + */ + public static final String AUTHORIZED_PARTY = "azp"; + + /** + * Audience Claim. From RFC 7519. + * https://tools.ietf.org/html/rfc7519#section-4.1.3 The "aud" (audience) claim + * identifies the recipients that the JWT is intended for. Each principal + * intended to process the JWT MUST identify itself with a value in the audience + * claim. If the principal processing the claim does not identify itself with a + * value in the "aud" claim when this claim is present, then the JWT MUST be + * rejected. In the general case, the "aud" value is an array of case- sensitive + * strings, each containing a StringOrURI value. In the special case when the + * JWT has one audience, the "aud" value MAY be a single case-sensitive string + * containing a StringOrURI value. The interpretation of audience values is + * generally application specific. Use of this claim is OPTIONAL. + */ + public static final String AUDIENCE_CLAIM = "aud"; + /** - * OAuth Url used to get a token from OAuthApiClient + * From RFC 7515 https://tools.ietf.org/html/rfc7515#section-4.1.4 The "kid" + * (key ID) Header Parameter is a hint indicating which key was used to secure + * the JWS. This parameter allows originators to explicitly signal a change of + * key to recipients. The structure of the "kid" value is unspecified. Its value + * MUST be a case-sensitive string. Use of this Header Parameter is OPTIONAL. + * When used with a JWK, the "kid" value is used to match a JWK "kid" parameter + * value. */ - public static final String OAuthUrl = "https://api.botframework.com"; + public static final String KEY_ID_HEADER = "kid"; + /** + * Service URL claim name. As used in Microsoft Bot Framework v3.1 auth. + */ + public static final String SERVICE_URL_CLAIM = "serviceurl"; + + /** + * Token version claim name. As used in Microsoft AAD tokens. + */ + public static final String VERSION_CLAIM = "ver"; + + /** + * App ID claim name. As used in Microsoft AAD 1.0 tokens. + */ + public static final String APPID_CLAIM = "appid"; + /** + * AppId used for creating skill claims when there is no appId and password configured. + */ + public static final String ANONYMOUS_SKILL_APPID = "AnonymousSkill"; + + /** + * Indicates anonymous (no app Id and password were provided). + */ + public static final String ANONYMOUS_AUTH_TYPE = "anonymous"; + + /** + * The default clock skew in minutes. + */ + public static final int DEFAULT_CLOCKSKEW_MINUTES = 5; static { - AllowedSigningAlgorithms.add("RS256"); - AllowedSigningAlgorithms.add("RS384"); - AllowedSigningAlgorithms.add("RS512"); + ALLOWED_SIGNING_ALGORITHMS.add("RS256"); + ALLOWED_SIGNING_ALGORITHMS.add("RS384"); + ALLOWED_SIGNING_ALGORITHMS.add("RS512"); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationException.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationException.java new file mode 100644 index 000000000..b876a5d29 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationException.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * Catchall exception for auth failures. + */ +public class AuthenticationException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Construct with exception. + * + * @param t The cause. + */ + public AuthenticationException(Throwable t) { + super(t); + } + + /** + * Construct with message. + * + * @param message The exception message. + */ + public AuthenticationException(String message) { + super(message); + } + + /** + * Construct with caught exception and message. + * + * @param message The message. + * @param t The caught exception. + */ + public AuthenticationException(String message, Throwable t) { + super(message, t); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/Authenticator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/Authenticator.java new file mode 100644 index 000000000..3c797d951 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/Authenticator.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.msal4j.IAuthenticationResult; +import java.util.concurrent.CompletableFuture; + +/** + * A provider of tokens. + */ +public interface Authenticator { + /** + * Returns a token. + * + * @return The MSAL token result. + */ + CompletableFuture acquireToken(); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotCredentials.java deleted file mode 100644 index 96666f997..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotCredentials.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -public class BotCredentials { - protected String appId; - protected String appPassword; - - public String appId() { return this.appId; } - - public String appPassword() { return this.appPassword; } - - public BotCredentials withAppId(String appId) { - this.appId = appId; - return this; - } - - public BotCredentials withAppPassword(String appPassword) { - this.appPassword = appPassword; - return this; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadata.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadata.java new file mode 100644 index 000000000..29c6f7dee --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadata.java @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkException; +import com.auth0.jwk.SigningKeyNotFoundException; +import com.auth0.jwk.UrlJwkProvider; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a cache of OpenID metadata keys. + */ +class CachingOpenIdMetadata implements OpenIdMetadata { + private static final Logger LOGGER = LoggerFactory.getLogger(CachingOpenIdMetadata.class); + private static final int CACHE_DAYS = 1; + private static final int CACHE_HOURS = 1; + + private String url; + private long lastUpdated; + private ObjectMapper mapper; + private Map keyCache = new HashMap<>(); + private final Object sync = new Object(); + + /** + * Constructs a OpenIdMetaData cache for a url. + * + * @param withUrl The url. + */ + CachingOpenIdMetadata(String withUrl) { + url = withUrl; + mapper = new ObjectMapper().findAndRegisterModules(); + } + + /** + * Gets a openid key. + * + *

+ * Note: This could trigger a cache refresh, which will incur network calls. + *

+ * + * @param keyId The JWT key. + * @return The cached key. + */ + @Override + public OpenIdMetadataKey getKey(String keyId) { + synchronized (sync) { + // If keys are more than CACHE_DAYS days old, refresh them + if (lastUpdated < System.currentTimeMillis() - Duration.ofDays(CACHE_DAYS).toMillis()) { + refreshCache(); + } + + // Search the cache even if we failed to refresh + OpenIdMetadataKey key = findKey(keyId); + if (key == null && lastUpdated < System.currentTimeMillis() - Duration.ofHours(CACHE_HOURS).toMillis()) { + // Refresh the cache if a key is not found (max once per CACHE_HOURS) + refreshCache(); + key = findKey(keyId); + } + return key; + } + } + + private void refreshCache() { + keyCache.clear(); + + try { + URL openIdUrl = new URL(this.url); + HashMap openIdConf = + this.mapper.readValue(openIdUrl, new TypeReference>() { + }); + URL keysUrl = new URL(openIdConf.get("jwks_uri").toString()); + lastUpdated = System.currentTimeMillis(); + UrlJwkProvider provider = new UrlJwkProvider(keysUrl); + keyCache = provider.getAll().stream().collect(Collectors.toMap(Jwk::getId, jwk -> jwk)); + } catch (IOException e) { + LOGGER.error(String.format("Failed to load openID config: %s", e.getMessage())); + lastUpdated = 0; + } catch (SigningKeyNotFoundException keyexception) { + LOGGER.error("refreshCache", keyexception); + lastUpdated = 0; + } + } + + @SuppressWarnings("unchecked") + private OpenIdMetadataKey findKey(String keyId) { + if (!keyCache.containsKey(keyId)) { + LOGGER.warn("findKey: keyId " + keyId + " doesn't exist."); + return null; + } + + try { + Jwk jwk = keyCache.get(keyId); + OpenIdMetadataKey key = new OpenIdMetadataKey(); + key.key = (RSAPublicKey) jwk.getPublicKey(); + key.endorsements = (List) jwk.getAdditionalAttributes().get("endorsements"); + key.certificateChain = jwk.getCertificateChain(); + return key; + } catch (JwkException e) { + String errorDescription = String.format("Failed to load keys: %s", e.getMessage()); + LOGGER.warn(errorDescription); + } + return null; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadataResolver.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadataResolver.java new file mode 100644 index 000000000..d937e37ff --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadataResolver.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Maintains a cache of OpenIdMetadata objects. + */ +public class CachingOpenIdMetadataResolver implements OpenIdMetadataResolver { + private static final ConcurrentMap OPENID_METADATA_CACHE = + new ConcurrentHashMap<>(); + + /** + * Gets the OpenIdMetadata object for the specified key. + * @param metadataUrl The key + * @return The OpenIdMetadata object. If the key is not found, an new OpenIdMetadata + * object is created. + */ + @Override + public OpenIdMetadata get(String metadataUrl) { + return OPENID_METADATA_CACHE + .computeIfAbsent(metadataUrl, key -> new CachingOpenIdMetadata(metadataUrl)); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java new file mode 100644 index 000000000..22af28a2a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +/** + * AppCredentials using a certificate. + */ +public class CertificateAppCredentials extends AppCredentials { + private Authenticator authenticator; + + /** + * Initializes a new instance of the AppCredentials class. + * + * @param withOptions The options for CertificateAppCredentials. + * @throws CertificateException During Authenticator creation. + * @throws UnrecoverableKeyException During Authenticator creation. + * @throws NoSuchAlgorithmException During Authenticator creation. + * @throws KeyStoreException During Authenticator creation. + * @throws NoSuchProviderException During Authenticator creation. + * @throws IOException During Authenticator creation. + */ + public CertificateAppCredentials(CertificateAppCredentialsOptions withOptions) + throws CertificateException, + UnrecoverableKeyException, + NoSuchAlgorithmException, + KeyStoreException, + NoSuchProviderException, + IOException { + + super(withOptions.getChannelAuthTenant(), withOptions.getoAuthScope()); + + // going to create this now instead of lazy loading so we don't have some + // awkward InputStream hanging around. + authenticator = + new CertificateAuthenticator(withOptions, new OAuthConfiguration(oAuthEndpoint(), oAuthScope())); + } + + /** + * Returns a CertificateAuthenticator. + * + * @return An Authenticator object. + */ + @Override + protected Authenticator buildAuthenticator() { + return authenticator; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentialsOptions.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentialsOptions.java new file mode 100644 index 000000000..174f71373 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentialsOptions.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import java.io.InputStream; + +/** + * CertificateAppCredentials Options. + */ +public class CertificateAppCredentialsOptions { + private String appId; + private String channelAuthTenant; + private String oAuthScope; + private InputStream pkcs12Certificate; + private String pkcs12Password; + private boolean sendX5c = true; + + /** + * Initializes the CertificateAppCredentialsOptions with the required arguments. + * + * @param withAppId The Microsoft app ID. + * @param withPkcs12Certificate The InputStream to the pkcs certificate. + * @param withPkcs12Password The pkcs certificate password. + */ + public CertificateAppCredentialsOptions( + String withAppId, + InputStream withPkcs12Certificate, + String withPkcs12Password + ) { + this(withAppId, withPkcs12Certificate, withPkcs12Password, null, null, true); + } + + /** + * Initializes the CertificateAppCredentialsOptions. + * + * @param withAppId The Microsoft app ID. + * @param withPkcs12Certificate The InputStream to the pkcs certificate. + * @param withPkcs12Password The pkcs certificate password. + * @param withChannelAuthTenant Optional. The oauth token tenant. + * @param withOAuthScope Optional. The scope for the token. + * @param withSendX5c Specifies if the x5c claim (public key of the + * certificate) should be sent to the STS. + */ + public CertificateAppCredentialsOptions( + String withAppId, + InputStream withPkcs12Certificate, + String withPkcs12Password, + String withChannelAuthTenant, + String withOAuthScope, + boolean withSendX5c + ) { + appId = withAppId; + channelAuthTenant = withChannelAuthTenant; + oAuthScope = withOAuthScope; + pkcs12Certificate = withPkcs12Certificate; + pkcs12Password = withPkcs12Password; + sendX5c = withSendX5c; + } + + /** + * Gets the Microsfot AppId. + * + * @return The app id. + */ + public String getAppId() { + return appId; + } + + /** + * Sets the Microsfot AppId. + * + * @param withAppId The app id. + */ + public void setAppId(String withAppId) { + appId = withAppId; + } + + /** + * Gets the Channel Auth Tenant. + * + * @return The OAuth Channel Auth Tenant. + */ + public String getChannelAuthTenant() { + return channelAuthTenant; + } + + /** + * Sets the Channel Auth Tenant. + * + * @param withChannelAuthTenant The OAuth Channel Auth Tenant. + */ + public void setChannelAuthTenant(String withChannelAuthTenant) { + channelAuthTenant = withChannelAuthTenant; + } + + /** + * Gets the OAuth scope. + * + * @return The OAuthScope. + */ + public String getoAuthScope() { + return oAuthScope; + } + + /** + * Sets the OAuth scope. + * + * @param withOAuthScope The OAuthScope. + */ + public void setoAuthScope(String withOAuthScope) { + oAuthScope = withOAuthScope; + } + + /** + * Gets the InputStream to the PKCS12 certificate. + * + * @return The InputStream to the certificate. + */ + public InputStream getPkcs12Certificate() { + return pkcs12Certificate; + } + + /** + * Sets the InputStream to the PKCS12 certificate. + * + * @param withPkcs12Certificate The InputStream to the certificate. + */ + public void setPkcs12Certificate(InputStream withPkcs12Certificate) { + pkcs12Certificate = withPkcs12Certificate; + } + + /** + * Gets the pkcs12 certiciate password. + * + * @return The password for the certificate. + */ + public String getPkcs12Password() { + return pkcs12Password; + } + + /** + * Sets the pkcs12 certiciate password. + * + * @param withPkcs12Password The password for the certificate. + */ + public void setPkcs12Password(String withPkcs12Password) { + pkcs12Password = withPkcs12Password; + } + + /** + * Gets if the x5c claim (public key of the certificate) should be sent to the + * STS. + * + * @return true to send x5c. + */ + public boolean getSendX5c() { + return sendX5c; + } + + /** + * Sets if the x5c claim (public key of the certificate) should be sent to the + * STS. + * + * @param withSendX5c true to send x5c. + */ + public void setSendX5c(boolean withSendX5c) { + sendX5c = withSendX5c; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAuthenticator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAuthenticator.java new file mode 100644 index 000000000..e83b90d71 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAuthenticator.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ClientCredentialParameters; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** + * A provider of tokens for CertificateAppCredentials. + */ +public class CertificateAuthenticator implements Authenticator { + private final ConfidentialClientApplication app; + private final ClientCredentialParameters parameters; + + /** + * Constructs an Authenticator using appId and pkcs certificate. + * + * @param withOptions The options for CertificateAppCredentials. + * @param withConfiguration The OAuthConfiguration. + * @throws CertificateException During MSAL app creation. + * @throws UnrecoverableKeyException During MSAL app creation. + * @throws NoSuchAlgorithmException During MSAL app creation. + * @throws KeyStoreException During MSAL app creation. + * @throws NoSuchProviderException During MSAL app creation. + * @throws IOException During MSAL app creation. + */ + public CertificateAuthenticator(CertificateAppCredentialsOptions withOptions, OAuthConfiguration withConfiguration) + throws CertificateException, + UnrecoverableKeyException, + NoSuchAlgorithmException, + KeyStoreException, + NoSuchProviderException, + IOException { + + app = ConfidentialClientApplication.builder( + withOptions.getAppId(), + ClientCredentialFactory.createFromCertificate( + withOptions.getPkcs12Certificate(), + withOptions.getPkcs12Password()) + ) + .authority(withConfiguration.getAuthority()).sendX5c(withOptions.getSendX5c()).build(); + + parameters = ClientCredentialParameters.builder(Collections.singleton(withConfiguration.getScope())).build(); + } + + /** + * Returns a token. + * + * @return The MSAL token result. + */ + @Override + public CompletableFuture acquireToken() { + return app.acquireToken(parameters) + .exceptionally( + exception -> { + // wrapping whatever msal throws into our own exception + throw new AuthenticationException(exception); + } + ); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelProvider.java new file mode 100644 index 000000000..20b29a5a7 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelProvider.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.CompletableFuture; + +/** + * ChannelProvider interface. This interface allows Bots to provide their own + * implementation for the configuration parameters to connect to a Bot. + * Framework channel service. + */ +public interface ChannelProvider { + /** + * Gets the channel service property for this channel provider. + * + * @return The channel service property for the channel provider. + */ + CompletableFuture getChannelService(); + + /** + * Gets a value of whether this provider represents a channel on Government + * Azure. + * + * @return True if this channel provider represents a channel on Government + * Azure. + */ + boolean isGovernment(); + + /** + * Gets a value of whether this provider represents a channel on Public Azure. + * + * @return True if this channel provider represents a channel on Public Azure. + */ + boolean isPublicAzure(); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java index 24c886e79..25b516bcd 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ChannelValidation.java @@ -3,92 +3,233 @@ package com.microsoft.bot.connector.authentication; -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.JwtTokenExtractor; +import org.apache.commons.lang3.StringUtils; +import java.time.Duration; +import java.util.ArrayList; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.*; +/** + * Channel auth validator. + */ +public final class ChannelValidation { + private static String openIdMetaDataUrl = + AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL; + + private ChannelValidation() { + + } -public class ChannelValidation { /** - * TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot + * TO BOT FROM CHANNEL. + * @return Token validation parameters when connecting to a bot. */ - public static final TokenValidationParameters ToBotFromChannelTokenValidationParameters = TokenValidationParameters.toBotFromChannelTokenValidationParameters(); + public static TokenValidationParameters getTokenValidationParameters() { + TokenValidationParameters tokenValidationParameters = new TokenValidationParameters(); + tokenValidationParameters.validateIssuer = true; + + ArrayList validIssuers = new ArrayList(); + validIssuers.add(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + tokenValidationParameters.validIssuers = validIssuers; + + tokenValidationParameters.validateAudience = false; + tokenValidationParameters.validateLifetime = true; + tokenValidationParameters.clockSkew = Duration.ofMinutes(AuthenticationConstants.DEFAULT_CLOCKSKEW_MINUTES); + tokenValidationParameters.requireSignedTokens = true; + + return tokenValidationParameters; + } /** - * Validate the incoming Auth Header as a token sent from the Bot Framework Service. - * @param authHeader The raw HTTP header in the format: "Bearer [longString]" - * @param credentials The user defined set of valid credentials, such as the AppId. - * @param channelId ChannelId for endorsements validation. - * @return A valid ClaimsIdentity. - * @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check. + * Gets the OpenID metadata URL. + * + * @return The url. */ - public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId) throws ExecutionException, InterruptedException, AuthenticationException { - JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( - ToBotFromChannelTokenValidationParameters, - ToBotFromChannelOpenIdMetadataUrl, - AllowedSigningAlgorithms); - - ClaimsIdentity identity = tokenExtractor.getIdentityAsync(authHeader, channelId).get(); - if (identity == null) { - // No valid identity. Not Authorized. - throw new AuthenticationException("Invalid Identity"); - } - - if (!identity.isAuthenticated()) { - // The token is in some way invalid. Not Authorized. - throw new AuthenticationException("Token Not Authenticated"); - } - - // Now check that the AppID in the claims set matches - // what we're looking for. Note that in a multi-tenant bot, this value - // comes from developer code that may be reaching out to a service, hence the - // Async validation. - - // Look for the "aud" claim, but only if issued from the Bot Framework - if (!identity.getIssuer().equalsIgnoreCase(ToBotFromChannelTokenIssuer)) { - throw new AuthenticationException("Token Not Authenticated"); - } - - // The AppId from the claim in the token must match the AppId specified by the developer. Note that - // the Bot Framework uses the Audience claim ("aud") to pass the AppID. - String appIdFromClaim = identity.claims().get(AudienceClaim); - if (appIdFromClaim == null || appIdFromClaim.isEmpty()) { - // Claim is present, but doesn't have a value. Not Authorized. - throw new AuthenticationException("Token Not Authenticated"); - } - - if (!credentials.isValidAppIdAsync(appIdFromClaim).get()) { - throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromClaim)); - } - - return CompletableFuture.completedFuture(identity); + public static String getOpenIdMetaDataUrl() { + return openIdMetaDataUrl; + } + + /** + * Sets the OpenID metadata URL. + * + * @param withOpenIdMetaDataUrl The metadata url. + */ + public static void setOpenIdMetaDataUrl(String withOpenIdMetaDataUrl) { + openIdMetaDataUrl = withOpenIdMetaDataUrl; } /** - * Validate the incoming Auth Header as a token sent from the Bot Framework Service. - * @param authHeader The raw HTTP header in the format: "Bearer [longString]" - * @param credentials The user defined set of valid credentials, such as the AppId. - * @param channelId ChannelId for endorsements validation. - * @param serviceUrl Service url. + * Validate the incoming Auth Header as a token sent from the Bot Framework + * Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelId ChannelId for endorsements validation. * @return A valid ClaimsIdentity. - * @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework emulator + * will FAIL this check. */ - public static CompletableFuture authenticateToken(String authHeader,CredentialProvider credentials, String channelId, String serviceUrl) throws ExecutionException, InterruptedException, AuthenticationException { - ClaimsIdentity identity = ChannelValidation.authenticateToken(authHeader, credentials, channelId).get(); + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + String channelId + ) { + return authenticateToken( + authHeader, credentials, channelId, new AuthenticationConfiguration() + ); + } - if (!identity.claims().containsKey(ServiceUrlClaim)) { - // Claim must be present. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim is required on Channel Token.", ServiceUrlClaim)); - } + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework + * Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelId ChannelId for endorsements validation. + * @param authConfig The AuthenticationConfiguration. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework emulator + * will FAIL this check. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + String channelId, + AuthenticationConfiguration authConfig + ) { + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + getTokenValidationParameters(), + getOpenIdMetaDataUrl(), + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ); + + return tokenExtractor.getIdentity(authHeader, channelId).thenCompose(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Invalid Identity"); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("Token Not Authenticated"); + } + + // Now check that the AppID in the claims set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + + // Look for the "aud" claim, but only if issued from the Bot Framework + if ( + !identity.getIssuer() + .equalsIgnoreCase(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER) + ) { + throw new AuthenticationException("Wrong Issuer"); + } + + // The AppId from the claim in the token must match the AppId specified by the + // developer. Note that + // the Bot Framework uses the Audience claim ("aud") to pass the AppID. + String appIdFromAudienceClaim = + identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appIdFromAudienceClaim)) { + // Claim is present, but doesn't have a value. Not Authorized. + throw new AuthenticationException("No Audience Claim"); + } + + return credentials.isValidAppId(appIdFromAudienceClaim).thenApply(isValid -> { + if (!isValid) { + throw new AuthenticationException( + String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim) + ); + } + return identity; + }); + }); + } - if (!serviceUrl.equalsIgnoreCase(identity.claims().get(ServiceUrlClaim))) { - // Claim must match. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim does not match service url provided (%s).", ServiceUrlClaim, serviceUrl)); - } + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework + * Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]" + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelId ChannelId for endorsements validation. + * @param serviceUrl Service url. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework emulator + * will FAIL this check. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + String channelId, + String serviceUrl + ) { + return authenticateToken( + authHeader, credentials, channelId, serviceUrl, new AuthenticationConfiguration() + ); + } - return CompletableFuture.completedFuture(identity); + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework + * Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]" + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelId ChannelId for endorsements validation. + * @param serviceUrl Service url. + * @param authConfig The authentication configuration. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework emulator + * will FAIL this check. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + String channelId, + String serviceUrl, + AuthenticationConfiguration authConfig + ) { + return ChannelValidation.authenticateToken(authHeader, credentials, channelId, authConfig) + .thenApply(identity -> { + if (!identity.claims().containsKey(AuthenticationConstants.SERVICE_URL_CLAIM)) { + // Claim must be present. Not Authorized. + throw new AuthenticationException( + String.format( + "'%s' claim is required on Channel Token.", + AuthenticationConstants.SERVICE_URL_CLAIM + ) + ); + } + + if ( + !serviceUrl.equalsIgnoreCase( + identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM) + ) + ) { + // Claim must match. Not Authorized. + throw new AuthenticationException( + String.format( + "'%s' claim does not match service url provided (%s).", + AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl + ) + ); + } + + return identity; + }); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java index d7aa764a1..00c88e90c 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java @@ -3,10 +3,102 @@ package com.microsoft.bot.connector.authentication; +import com.auth0.jwt.interfaces.DecodedJWT; + +import java.util.HashMap; import java.util.Map; -public interface ClaimsIdentity { - boolean isAuthenticated(); - Map claims(); - String getIssuer(); +/** + * This is a simple wrapper around for a JWT claims identity. + */ +public class ClaimsIdentity { + private String issuer; + private String type; + private Map claims; + + private ClaimsIdentity() { + this("", new HashMap<>()); + } + + /** + * Manually construct with auth issuer. + * + * @param withAuthIssuer The auth issuer. + */ + public ClaimsIdentity(String withAuthIssuer) { + this(withAuthIssuer, new HashMap<>()); + } + + /** + * Manually construct with issuer and claims. + * + * @param withAuthIssuer The auth issuer. + * @param withClaims A Map of claims. + */ + public ClaimsIdentity(String withAuthIssuer, Map withClaims) { + this(withAuthIssuer, null, withClaims); + } + + /** + * Manually construct with issuer and claims. + * + * @param withAuthIssuer The auth issuer. + * @param withType The auth type. + * @param withClaims A Map of claims. + */ + public ClaimsIdentity(String withAuthIssuer, String withType, Map withClaims) { + issuer = withAuthIssuer; + type = withType; + claims = withClaims; + } + + /** + * Extract data from an auth0 JWT. + * + * @param jwt The decoded JWT. + */ + public ClaimsIdentity(DecodedJWT jwt) { + claims = new HashMap<>(); + if (jwt.getClaims() != null) { + jwt.getClaims().forEach((k, v) -> claims.put(k, v.asString())); + } + issuer = jwt.getIssuer(); + type = jwt.getType(); + } + + /** + * Gets whether the claim is authenticated. + * + * @return true if authenticated. + */ + public boolean isAuthenticated() { + return this.issuer != null && !this.issuer.isEmpty(); + } + + /** + * The claims for this identity. + * + * @return A Map of claims. + */ + public Map claims() { + return this.claims; + } + + /** + * The issuer. + * + * @return The issuer. + */ + public String getIssuer() { + return issuer; + } + + /** + * The type. + * + * @return The type. + */ + public String getType() { + return type; + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentityImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentityImpl.java deleted file mode 100644 index 1b7f66d84..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentityImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -import java.util.HashMap; -import java.util.Map; - -public class ClaimsIdentityImpl implements ClaimsIdentity { - private String issuer; - private Map claims; - - public ClaimsIdentityImpl() { - this("", new HashMap<>()); - } - - public ClaimsIdentityImpl(String authType) { - this(authType, new HashMap<>()); - } - - public ClaimsIdentityImpl(String authType, Map claims) { - this.issuer = authType; - this.claims = claims; - } - - @Override - public boolean isAuthenticated() { - return this.issuer != null && !this.issuer.isEmpty(); - } - - @Override - public Map claims() { - return this.claims; - } - - @Override - public String getIssuer() { - return issuer; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsValidator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsValidator.java new file mode 100644 index 000000000..42692da1a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsValidator.java @@ -0,0 +1,20 @@ +package com.microsoft.bot.connector.authentication; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * An abstract class used to validate identity. + */ +public abstract class ClaimsValidator { + + /** + * Validates a Map of claims and should throw an exception if the + * validation fails. + * + * @param claims The Map of claims to validate. + * + * @return true if the validation is successful, false if not. + */ + public abstract CompletableFuture validateClaims(Map claims); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProvider.java index 61b60976f..0c54cf0ec 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProvider.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProvider.java @@ -5,8 +5,46 @@ import java.util.concurrent.CompletableFuture; +/** + * CredentialProvider interface. This interface allows Bots to provide their own + * implementation of what is, and what is not, a valid appId and password. This + * is useful in the case of multi-tenant bots, where the bot may need to call + * out to a service to determine if a particular appid/password pair is valid. + * + * For Single Tenant bots (the vast majority) the simple static providers are + * sufficient. + */ public interface CredentialProvider { - CompletableFuture isValidAppIdAsync(String appId); - CompletableFuture getAppPasswordAsync(String appId); - CompletableFuture isAuthenticationDisabledAsync(); + /** + * Validates an app ID. + * + * @param appId The app ID to validate. + * @return A task that represents the work queued to execute. If the task is + * successful, the result is true if appId is valid for the controller; + * otherwise, false. + */ + CompletableFuture isValidAppId(String appId); + + /** + * Gets the app password for a given bot app ID. + * + * @param appId The ID of the app to get the password for. + * @return A task that represents the work queued to execute. If the task is + * successful and the app ID is valid, the result contains the password; + * otherwise, null. This method is async to enable custom + * implementations that may need to call out to serviced to validate the + * appId / password pair. + */ + CompletableFuture getAppPassword(String appId); + + /** + * Checks whether bot authentication is disabled. + * + * @return A task that represents the work queued to execute. If the task is + * successful and bot authentication is disabled, the result is true; + * otherwise, false. This method is async to enable custom + * implementations that may need to call out to serviced to validate the + * appId / password pair. + */ + CompletableFuture isAuthenticationDisabled(); } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProviderImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProviderImpl.java deleted file mode 100644 index 2d6eb1e01..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialProviderImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -import java.util.concurrent.CompletableFuture; - -public class CredentialProviderImpl extends BotCredentials implements CredentialProvider { - - public CredentialProviderImpl(String appId, String appPassword) { - this.appId = appId; - this.appPassword = appPassword; - } - - public CredentialProviderImpl(BotCredentials credentials) { - this(credentials.appId, credentials.appPassword); - } - - @Override - public CredentialProviderImpl withAppId(String appId) { - return (CredentialProviderImpl) super.withAppId(appId); - } - - @Override - public CredentialProviderImpl withAppPassword(String appPassword) { - return (CredentialProviderImpl) super.withAppPassword(appPassword); - } - - @Override - public CompletableFuture isValidAppIdAsync(String appId) { - return CompletableFuture.completedFuture(this.appId.equals(appId)); - } - - @Override - public CompletableFuture getAppPasswordAsync(String appId) { - return CompletableFuture.completedFuture((this.appId.equals(appId) ? this.appPassword : null)); - } - - @Override - public CompletableFuture isAuthenticationDisabledAsync() { - return CompletableFuture.completedFuture(this.appId == null || this.appId.isEmpty()); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialsAuthenticator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialsAuthenticator.java new file mode 100644 index 000000000..9727335b8 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CredentialsAuthenticator.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ClientCredentialParameters; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.MsalServiceException; + +import java.net.MalformedURLException; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** + * An Authenticator using app id and password. + */ +public class CredentialsAuthenticator implements Authenticator { + private final ConfidentialClientApplication app; + private final ClientCredentialParameters parameters; + + /** + * Constructs an Authenticator using appId and appPassword. + * + * @param appId The app id. + * @param appPassword The app password. + * @param configuration The OAuthConfiguration. + * @throws MalformedURLException Invalid endpoint. + */ + CredentialsAuthenticator(String appId, String appPassword, OAuthConfiguration configuration) + throws MalformedURLException { + + app = ConfidentialClientApplication.builder(appId, ClientCredentialFactory.createFromSecret(appPassword)) + .authority(configuration.getAuthority()).build(); + + parameters = ClientCredentialParameters.builder(Collections.singleton(configuration.getScope())).build(); + } + + /** + * Gets an auth result via MSAL. + * + * @return The auth result. + */ + @Override + public CompletableFuture acquireToken() { + return Retry.run(() -> app.acquireToken(parameters).exceptionally(exception -> { + // wrapping whatever msal throws into our own exception + throw new AuthenticationException(exception); + }), (exception, count) -> { + if (exception instanceof RetryException && exception.getCause() instanceof MsalServiceException) { + MsalServiceException serviceException = (MsalServiceException) exception.getCause(); + if (serviceException.headers().containsKey("Retry-After")) { + return RetryAfterHelper.processRetry(serviceException.headers().get("Retry-After"), count); + } else { + return RetryParams.defaultBackOff(++count); + } + } + return RetryParams.stopRetrying(); + }); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java index 1a9bb9192..02da3a176 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java @@ -5,40 +5,71 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.DecodedJWT; -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.JwtTokenExtractor; +import org.apache.commons.lang3.StringUtils; +import java.time.Duration; +import java.util.ArrayList; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.*; /** - * Validates and Examines JWT tokens from the Bot Framework Emulator + * Validates and Examines JWT tokens from the Bot Framework Emulator. */ -public class EmulatorValidation { +public final class EmulatorValidation { + private EmulatorValidation() { + + } + /** - * TO BOT FROM EMULATOR: Token validation parameters when connecting to a channel. + * TO BOT FROM EMULATOR. + * @return Token validation parameters when connecting to a channel. */ - public static final TokenValidationParameters ToBotFromEmulatorTokenValidationParameters = TokenValidationParameters.toBotFromEmulatorTokenValidationParameters(); + public static TokenValidationParameters getTokenValidationParameters() { + TokenValidationParameters tokenValidationParameters = new TokenValidationParameters(); + tokenValidationParameters.validateIssuer = true; + + ArrayList validIssuers = new ArrayList(); + // Auth v3.1, 1.0 + validIssuers.add("https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/"); + // Auth v3.1, 2.0 + validIssuers.add("https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0"); + // Auth v3.2, 1.0 + validIssuers.add("https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/"); + // Auth v3.2, 2.0 + validIssuers.add("https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0"); + // Auth for US Gov, 1.0 + validIssuers.add("https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/"); + // Auth for US Gov, 2.0 + validIssuers.add("https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0"); + tokenValidationParameters.validIssuers = validIssuers; + + tokenValidationParameters.validateAudience = false; + tokenValidationParameters.validateLifetime = true; + tokenValidationParameters.clockSkew = Duration.ofMinutes(AuthenticationConstants.DEFAULT_CLOCKSKEW_MINUTES); + + tokenValidationParameters.requireSignedTokens = true; + + return tokenValidationParameters; + } /** - * Determines if a given Auth header is from the Bot Framework Emulator + * Determines if a given Auth header is from the Bot Framework Emulator. + * * @param authHeader Bearer Token, in the "Bearer [Long String]" Format. * @return True, if the token was issued by the Emulator. Otherwise, false. */ - public static CompletableFuture isTokenFromEmulator(String authHeader) { + public static Boolean isTokenFromEmulator(String authHeader) { // The Auth Header generally looks like this: // "Bearer eyJ0e[...Big Long String...]XAiO" - if (authHeader == null || authHeader.isEmpty()) { + if (StringUtils.isEmpty(authHeader)) { // No token. Can't be an emulator token. - return CompletableFuture.completedFuture(false); + return false; } String[] parts = authHeader.split(" "); if (parts.length != 2) { - // Emulator tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not an emulator token - return CompletableFuture.completedFuture(false); + // Emulator tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not + // an emulator token + return false; } String schema = parts[0]; @@ -46,91 +77,165 @@ public static CompletableFuture isTokenFromEmulator(String authHeader) if (!schema.equalsIgnoreCase("bearer")) { // The scheme from the emulator MUST be "Bearer" - return CompletableFuture.completedFuture(false); + return false; } // Parse the Big Long String into an actual token. - DecodedJWT decodedJWT = JWT.decode(token); + try { + DecodedJWT decodedJWT = JWT.decode(token); - // Is there an Issuer? - if (decodedJWT.getIssuer().isEmpty()) { - // No Issuer, means it's not from the Emulator. - return CompletableFuture.completedFuture(false); - } + // Is there an Issuer? + if (StringUtils.isEmpty(decodedJWT.getIssuer())) { + // No Issuer, means it's not from the Emulator. + return false; + } - // Is the token issues by a source we consider to be the emulator? - if (!ToBotFromEmulatorTokenValidationParameters.validIssuers.contains(decodedJWT.getIssuer())) { + // Is the token issues by a source we consider to be the emulator? // Not a Valid Issuer. This is NOT a Bot Framework Emulator Token. - return CompletableFuture.completedFuture(false); + return getTokenValidationParameters().validIssuers.contains(decodedJWT.getIssuer()); + } catch (Throwable t) { + return false; } - - // The Token is from the Bot Framework Emulator. Success! - return CompletableFuture.completedFuture(true); } /** - * Validate the incoming Auth Header as a token sent from the Bot Framework Emulator. - * @param authHeader The raw HTTP header in the format: "Bearer [longString]" - * @param credentials The user defined set of valid credentials, such as the AppId. + * Validate the incoming Auth Header as a token sent from the Bot Framework + * Emulator. A token issued by the Bot Framework will FAIL this check. Only + * Emulator tokens will pass. + * + * @param authHeader The raw HTTP header in the format: "Bearer + * [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelProvider The channelService value that distinguishes public + * Azure from US Government Azure. + * @param channelId The ID of the channel to validate. * @return A valid ClaimsIdentity. - * @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + *

+ * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL + * this check. Only Emulator tokens will pass. */ - public static CompletableFuture authenticateToken(String authHeader, CredentialProvider credentials, String channelId) throws ExecutionException, InterruptedException, AuthenticationException { - JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( - ToBotFromEmulatorTokenValidationParameters, - ToBotFromEmulatorOpenIdMetadataUrl, - AllowedSigningAlgorithms); - - ClaimsIdentity identity = tokenExtractor.getIdentityAsync(authHeader, channelId).get(); - if (identity == null) { - // No valid identity. Not Authorized. - throw new AuthenticationException("Invalid Identity"); - } - - if (!identity.isAuthenticated()) { - // The token is in some way invalid. Not Authorized. - throw new AuthenticationException("Token Not Authenticated"); - } - - // Now check that the AppID in the claims set matches - // what we're looking for. Note that in a multi-tenant bot, this value - // comes from developer code that may be reaching out to a service, hence the - // Async validation. - if (!identity.claims().containsKey(VersionClaim)) { - throw new AuthenticationException(String.format("'%s' claim is required on Emulator Tokens.", VersionClaim)); - } - - String tokenVersion = identity.claims().get(VersionClaim); - String appId = ""; - - // The Emulator, depending on Version, sends the AppId via either the - // appid claim (Version 1) or the Authorized Party claim (Version 2). - if (tokenVersion.isEmpty() || tokenVersion.equalsIgnoreCase("1.0")) { - // either no Version or a version of "1.0" means we should look for - // the claim in the "appid" claim. - if (!identity.claims().containsKey(AppIdClaim)) { - // No claim around AppID. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '1.0'.", AppIdClaim)); - } - - appId = identity.claims().get(AppIdClaim); - } else if (tokenVersion.equalsIgnoreCase("2.0")) { - // Emulator, "2.0" puts the AppId in the "azp" claim. - if (!identity.claims().containsKey(AuthorizedParty)) { - // No claim around AppID. Not Authorized. - throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '2.0'.", AuthorizedParty)); - } - - appId = identity.claims().get(AuthorizedParty); - } else { - // Unknown Version. Not Authorized. - throw new AuthenticationException(String.format("Unknown Emulator Token version '%s'.", tokenVersion)); - } + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String channelId + ) { + return authenticateToken( + authHeader, credentials, channelProvider, channelId, new AuthenticationConfiguration() + ); + } - if (!credentials.isValidAppIdAsync(appId).get()) { - throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appId)); - } + /** + * Validate the incoming Auth Header as a token sent from the Bot Framework + * Emulator. A token issued by the Bot Framework will FAIL this check. Only + * Emulator tokens will pass. + * + * @param authHeader The raw HTTP header in the format: "Bearer + * [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelProvider The channelService value that distinguishes public + * Azure from US Government Azure. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * @return A valid ClaimsIdentity. + *

+ * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL + * this check. Only Emulator tokens will pass. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String channelId, + AuthenticationConfiguration authConfig + ) { + String openIdMetadataUrl = channelProvider != null && channelProvider.isGovernment() + ? GovernmentAuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL + : AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL; - return CompletableFuture.completedFuture(identity); + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + getTokenValidationParameters(), + openIdMetadataUrl, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ); + + return tokenExtractor.getIdentity(authHeader, channelId, authConfig.requiredEndorsements()) + .thenCompose(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Invalid Identity"); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("Token Not Authenticated"); + } + + // Now check that the AppID in the claims set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + if (!identity.claims().containsKey(AuthenticationConstants.VERSION_CLAIM)) { + throw new AuthenticationException( + String.format( + "'%s' claim is required on Emulator Tokens.", + AuthenticationConstants.VERSION_CLAIM + ) + ); + } + + String tokenVersion = identity.claims().get(AuthenticationConstants.VERSION_CLAIM); + String appId; + + // The Emulator, depending on Version, sends the AppId via either the + // appid claim (Version 1) or the Authorized Party claim (Version 2). + if (StringUtils.isEmpty(tokenVersion) || tokenVersion.equalsIgnoreCase("1.0")) { + // either no Version or a version of "1.0" means we should look for + // the claim in the "appid" claim. + if (!identity.claims().containsKey(AuthenticationConstants.APPID_CLAIM)) { + // No claim around AppID. Not Authorized. + throw new AuthenticationException( + String.format( + "'%s' claim is required on Emulator Token version '1.0'.", + AuthenticationConstants.APPID_CLAIM + ) + ); + } + + appId = identity.claims().get(AuthenticationConstants.APPID_CLAIM); + } else if (tokenVersion.equalsIgnoreCase("2.0")) { + // Emulator, "2.0" puts the AppId in the "azp" claim. + if (!identity.claims().containsKey(AuthenticationConstants.AUTHORIZED_PARTY)) { + // No claim around AppID. Not Authorized. + throw new AuthenticationException( + String.format( + "'%s' claim is required on Emulator Token version '2.0'.", + AuthenticationConstants.AUTHORIZED_PARTY + ) + ); + } + + appId = identity.claims().get(AuthenticationConstants.AUTHORIZED_PARTY); + } else { + // Unknown Version. Not Authorized. + throw new AuthenticationException( + String.format("Unknown Emulator Token version '%s'.", tokenVersion) + ); + } + + return credentials.isValidAppId(appId).thenApply(isValid -> { + if (!isValid) { + throw new AuthenticationException( + String.format("Invalid AppId passed on token: '%s'.", appId) + ); + } + + return identity; + }); + }); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java index 4182b11c0..fe9dffabc 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EndorsementsValidator.java @@ -1,46 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.authentication; +import org.apache.commons.lang3.StringUtils; + import java.util.List; +/** + * Verify that the specified endorsement exists on the JWT token. + */ public abstract class EndorsementsValidator { - /** - * Verify that the set of ChannelIds, which come from the incoming activities, - * all match the endorsements found on the JWT Token. - * For example, if an Activity comes from webchat, that channelId says - * says "webchat" and the jwt token endorsement MUST match that. - * @param channelId The channel name, typically extracted from the activity.ChannelId field, that - * to which the Activity is affinitized. - * @param endorsements Whoever signed the JWT token is permitted to send activities only for - * some specific channels. That list is the endorsement list, and is validated here against the channelId. - * @return True is the channelId is found in the Endorsement set. False if the channelId is not found. + * Verify that the specified endorsement exists on the JWT token. Call this + * method multiple times to validate multiple endorsements. + * + *

+ * For example, if an {@link com.microsoft.bot.schema.Activity} comes from + * WebChat, that activity's + * {@link com.microsoft.bot.schema.Activity#getChannelId()} property is set to + * "webchat" and the signing party of the JWT token must have a corresponding + * endorsement of “Webchat”. + *

+ * + * @param expectedEndorsement The expected endorsement. Generally the ID of the + * channel to validate, typically extracted from the + * activity's + * {@link com.microsoft.bot.schema.Activity#getChannelId()} + * property, that to which the Activity is + * affinitized. Alternatively, it could represent a + * compliance certification that is required. + * @param endorsements The JWT token’s signing party is permitted to send + * activities only for specific channels. That list, + * the set of channels the service can sign for, is + * called the endorsement list. The activity’s + * Schema.Activity.ChannelId MUST be found in the + * endorsement list, or the incoming activity is not + * considered valid. + * @return True is the expected endorsement is found in the Endorsement set. + * @throws IllegalArgumentException Missing endorsements */ - public static boolean validate(String channelId, List endorsements) { + public static boolean validate(String expectedEndorsement, List endorsements) + throws IllegalArgumentException { // If the Activity came in and doesn't have a Channel ID then it's making no // assertions as to who endorses it. This means it should pass. - if (channelId == null || channelId.isEmpty()) + if (StringUtils.isEmpty(expectedEndorsement)) { return true; + } - if (endorsements == null) + if (endorsements == null) { throw new IllegalArgumentException("endorsements must be present."); + } // The Call path to get here is: // JwtTokenValidation.authenticateRequest - // -> - // JwtTokenValidation.validateAuthHeader - // -> - // ChannelValidation.authenticateToken - // -> - // JwtTokenExtractor - - // Does the set of endorsements match the channelId that was passed in? - - // ToDo: Consider moving this to a HashSet instead of a string - // array, to make lookups O(1) instead of O(N). To give a sense - // of scope, tokens from WebChat have about 10 endorsements, and - // tokens coming from Teams have about 20. + // -> + // JwtTokenValidation.validateAuthHeader + // -> + // ChannelValidation.authenticateToken + // -> + // JwtTokenExtractor - return endorsements.contains(channelId); + // Does the set of endorsements match the expected endorsement that was passed + // in? + return endorsements.contains(expectedEndorsement); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EnterpriseChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EnterpriseChannelValidation.java new file mode 100644 index 000000000..70d6f9a51 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EnterpriseChannelValidation.java @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.bot.connector.Async; +import org.apache.commons.lang3.StringUtils; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +/** + * Enterprise channel auth validation. + */ +public final class EnterpriseChannelValidation { + private static TokenValidationParameters getTokenValidationParameters() { + TokenValidationParameters tokenValidationParamaters = new TokenValidationParameters(); + tokenValidationParamaters.validateIssuer = true; + + ArrayList validIssuers = new ArrayList(); + validIssuers.add(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + tokenValidationParamaters.validIssuers = validIssuers; + + tokenValidationParamaters.validateAudience = false; + tokenValidationParamaters.validateLifetime = true; + tokenValidationParamaters.clockSkew = Duration.ofMinutes(AuthenticationConstants.DEFAULT_CLOCKSKEW_MINUTES); + tokenValidationParamaters.requireSignedTokens = true; + + return tokenValidationParamaters; + } + + private EnterpriseChannelValidation() { + + } + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework + * Channel Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer + * [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelProvider The channelService value that distinguishes public + * Azure from US Government Azure. + * @param serviceUrl The service url from the request. + * @param channelId The ID of the channel to validate. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL + * this check. Only Emulator tokens will pass. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String serviceUrl, + String channelId + ) { + return authenticateToken( + authHeader, credentials, channelProvider, serviceUrl, channelId, + new AuthenticationConfiguration() + ); + } + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework + * Channel Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer + * [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelProvider The channelService value that distinguishes public + * Azure from US Government Azure. + * @param serviceUrl The service url from the request. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * @return A valid ClaimsIdentity. + * @throws AuthenticationException A token issued by the Bot Framework will FAIL + * this check. Only Emulator tokens will pass. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String serviceUrl, + String channelId, + AuthenticationConfiguration authConfig + ) { + if (authConfig == null) { + return Async.completeExceptionally(new IllegalArgumentException("Missing AuthenticationConfiguration")); + } + + return channelProvider.getChannelService() + + .thenCompose(channelService -> { + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + getTokenValidationParameters(), + String.format( + AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPENID_METADATA_URL_FORMAT, + channelService + ), + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ); + + return tokenExtractor + .getIdentity(authHeader, channelId, authConfig.requiredEndorsements()); + }) + + .thenCompose(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Invalid Identity"); + } + + return validateIdentity(identity, credentials, serviceUrl); + }); + } + + /** + * Validates a {@link ClaimsIdentity}. + * + * @param identity The ClaimsIdentity to validate. + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param serviceUrl The service url from the request. + * @return A valid ClaimsIdentity. + * + * On join: + * @throws AuthenticationException A token issued by the Bot Framework will FAIL + * this check. Only Emulator tokens will pass. + */ + public static CompletableFuture validateIdentity( + ClaimsIdentity identity, + CredentialProvider credentials, + String serviceUrl + ) { + + CompletableFuture result = new CompletableFuture<>(); + + // Validate the identity + + if (identity == null || !identity.isAuthenticated()) { + result.completeExceptionally(new AuthenticationException("Invalid Identity")); + return result; + } + + if ( + !StringUtils.equalsIgnoreCase( + identity.getIssuer(), AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER + ) + ) { + + result.completeExceptionally(new AuthenticationException("Wrong Issuer")); + return result; + } + + // The AppId from the claim in the token must match the AppId specified by the + // developer. Note that + // the Bot Framework uses the Audience claim ("aud") to pass the AppID. + String appIdFromAudienceClaim = + identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appIdFromAudienceClaim)) { + // Claim is present, but doesn't have a value. Not Authorized. + result.completeExceptionally(new AuthenticationException("No Audience Claim")); + return result; + } + + // Now check that the AppID in the claim set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + + return credentials.isValidAppId(appIdFromAudienceClaim).thenApply(isValid -> { + if (!isValid) { + throw new AuthenticationException( + String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim) + ); + } + + String serviceUrlClaim = + identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM); + if (StringUtils.isEmpty(serviceUrl)) { + throw new AuthenticationException( + String.format("Invalid serviceurl passed on token: '%s'.", serviceUrlClaim) + ); + } + + if (!StringUtils.equals(serviceUrl, serviceUrlClaim)) { + throw new AuthenticationException( + String.format("serviceurl doesn't match claim: '%s'.", serviceUrlClaim) + ); + } + + return identity; + }); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentAuthenticationConstants.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentAuthenticationConstants.java new file mode 100644 index 000000000..be0f12312 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentAuthenticationConstants.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * Values and Constants used for Authentication and Authorization by the Bot + * Framework Protocol to US Government DataCenters. + */ +public final class GovernmentAuthenticationConstants { + private GovernmentAuthenticationConstants() { + + } + + public static final String CHANNELSERVICE = "https://botframework.azure.us"; + + /** + * TO GOVERNMENT CHANNEL FROM BOT: Login URL. + */ + public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL = + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e"; + + /** + * TO GOVERNMENT CHANNEL FROM BOT: OAuth scope to request. + */ + public static final String TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = + "https://api.botframework.us/.default"; + + /** + * TO BOT FROM GOVERNMENT CHANNEL: Token issuer. + */ + public static final String TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.us"; + + /** + * OAuth Url used to get a token from OAuthApiClient. + */ + public static final String OAUTH_URL_GOV = "https://api.botframework.azure.us"; + + /** + * TO BOT FROM GOVERNMANT CHANNEL: OpenID metadata document for tokens coming + * from MSA. + */ + public static final String TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = + "https://login.botframework.azure.us/v1/.well-known/openidconfiguration"; + + /** + * TO BOT FROM GOVERNMENT EMULATOR: OpenID metadata document for tokens coming + * from MSA. + */ + public static final String TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration"; +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java new file mode 100644 index 000000000..cb2de0305 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import org.apache.commons.lang3.StringUtils; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +/** + * Government Channel auth validation. + */ +public final class GovernmentChannelValidation { + private static String openIdMetaDataUrl = + GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL; + + /** + * TO BOT FROM GOVERNMENT CHANNEL. + * @return Token validation parameters when connecting to a bot. + */ + public static TokenValidationParameters getTokenValidationParameters() { + TokenValidationParameters tokenValidationParameters = new TokenValidationParameters(); + + ArrayList validIssuers = new ArrayList(); + tokenValidationParameters.validIssuers = validIssuers; + + tokenValidationParameters.validateIssuer = true; + tokenValidationParameters.validateAudience = false; + tokenValidationParameters.validateLifetime = true; + tokenValidationParameters.clockSkew = Duration.ofMinutes(AuthenticationConstants.DEFAULT_CLOCKSKEW_MINUTES); + tokenValidationParameters.requireSignedTokens = true; + + return tokenValidationParameters; + } + + private GovernmentChannelValidation() { + + } + + /** + * Gets the OpenID metadata URL. + * + * @return The url. + */ + public static String getOpenIdMetaDataUrl() { + return openIdMetaDataUrl; + } + + /** + * Sets the OpenID metadata URL. + * + * @param withOpenIdMetaDataUrl The metadata url. + */ + public static void setOpenIdMetaDataUrl(String withOpenIdMetaDataUrl) { + openIdMetaDataUrl = withOpenIdMetaDataUrl; + } + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework + * Government Channel Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param serviceUrl The service url from the request. + * @param channelId The ID of the channel to validate. + * @return A CompletableFuture representing the asynchronous operation. + * + * On join: + * @throws AuthenticationException Authentication failed. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + String serviceUrl, + String channelId + ) { + return authenticateToken( + authHeader, credentials, serviceUrl, channelId, new AuthenticationConfiguration() + ); + } + + /** + * Validate the incoming Auth Header as a token sent from a Bot Framework + * Government Channel Service. + * + * @param authHeader The raw HTTP header in the format: "Bearer [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param serviceUrl The service url from the request. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * @return A CompletableFuture representing the asynchronous operation. + * + * On join: + * @throws AuthenticationException Authentication failed. + */ + public static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + String serviceUrl, + String channelId, + AuthenticationConfiguration authConfig + ) { + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + getTokenValidationParameters(), + getOpenIdMetaDataUrl(), + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ); + + return tokenExtractor.getIdentity(authHeader, channelId, authConfig.requiredEndorsements()) + .thenCompose(identity -> validateIdentity(identity, credentials, serviceUrl)); + } + + /** + * Validate the ClaimsIdentity as sent from a Bot Framework Government Channel + * Service. + * + * @param identity The claims identity to validate. + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param serviceUrl The service url from the request. + * @return A CompletableFuture representing the asynchronous operation. + * + * On join: + * @throws AuthenticationException Validation failed. + */ + public static CompletableFuture validateIdentity( + ClaimsIdentity identity, + CredentialProvider credentials, + String serviceUrl + ) { + + CompletableFuture result = new CompletableFuture<>(); + + if (identity == null || !identity.isAuthenticated()) { + result.completeExceptionally(new AuthenticationException("Invalid Identity")); + return result; + } + + if ( + !StringUtils.equalsIgnoreCase( + identity.getIssuer(), + GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER + ) + ) { + + result.completeExceptionally(new AuthenticationException("Wrong Issuer")); + return result; + } + + // The AppId from the claim in the token must match the AppId specified by the + // developer. Note that + // the Bot Framework uses the Audience claim ("aud") to pass the AppID. + String appIdFromAudienceClaim = + identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isEmpty(appIdFromAudienceClaim)) { + // Claim is present, but doesn't have a value. Not Authorized. + result.completeExceptionally(new AuthenticationException("No Audience Claim")); + return result; + } + + // Now check that the AppID in the claim set matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + + return credentials.isValidAppId(appIdFromAudienceClaim).thenApply(isValid -> { + if (!isValid) { + throw new AuthenticationException( + String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim) + ); + } + + String serviceUrlClaim = + identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM); + if (StringUtils.isEmpty(serviceUrl)) { + throw new AuthenticationException( + String.format("Invalid serviceurl passed on token: '%s'.", serviceUrlClaim) + ); + } + + if (!StringUtils.equals(serviceUrl, serviceUrlClaim)) { + throw new AuthenticationException( + String.format("serviceurl doesn't match claim: '%s'.", serviceUrlClaim) + ); + } + + return identity; + }); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java index b49f78a7d..5e564d324 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java @@ -8,109 +8,226 @@ import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.ClaimsIdentity; -import com.microsoft.bot.connector.authentication.ClaimsIdentityImpl; -import com.microsoft.bot.connector.authentication.TokenValidationParameters; +import com.microsoft.bot.connector.ExecutorFactory; +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Date; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; +/** + * Extracts relevant data from JWT Tokens. + */ public class JwtTokenExtractor { - private static final Logger LOGGER = Logger.getLogger(OpenIdMetadata.class.getName()); - - private static final ConcurrentMap openIdMetadataCache = new ConcurrentHashMap<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(CachingOpenIdMetadata.class); private TokenValidationParameters tokenValidationParameters; private List allowedSigningAlgorithms; + private OpenIdMetadataResolver openIdMetadataResolver; private OpenIdMetadata openIdMetadata; - public JwtTokenExtractor(TokenValidationParameters tokenValidationParameters, String metadataUrl, List allowedSigningAlgorithms) { - this.tokenValidationParameters = new TokenValidationParameters(tokenValidationParameters); + /** + * Initializes a new instance of the JwtTokenExtractor class. + * + * @param withTokenValidationParameters tokenValidationParameters. + * @param withMetadataUrl metadataUrl. + * @param withAllowedSigningAlgorithms allowedSigningAlgorithms. + */ + public JwtTokenExtractor( + TokenValidationParameters withTokenValidationParameters, + String withMetadataUrl, + List withAllowedSigningAlgorithms + ) { + this.tokenValidationParameters = + new TokenValidationParameters(withTokenValidationParameters); this.tokenValidationParameters.requireSignedTokens = true; - this.allowedSigningAlgorithms = allowedSigningAlgorithms; - this.openIdMetadata = openIdMetadataCache.computeIfAbsent(metadataUrl, key -> new OpenIdMetadata(metadataUrl)); + this.allowedSigningAlgorithms = withAllowedSigningAlgorithms; + + if (tokenValidationParameters.issuerSigningKeyResolver == null) { + this.openIdMetadataResolver = new CachingOpenIdMetadataResolver(); + } else { + this.openIdMetadataResolver = tokenValidationParameters.issuerSigningKeyResolver; + } + + this.openIdMetadata = this.openIdMetadataResolver.get(withMetadataUrl); + } + + /** + * Get a ClaimsIdentity from an auth header and channel id. + * + * @param authorizationHeader The Authorization header value. + * @param channelId The channel id. + * @return A ClaimsIdentity if successful. + */ + public CompletableFuture getIdentity( + String authorizationHeader, + String channelId + ) { + return getIdentity(authorizationHeader, channelId, new ArrayList<>()); } - public CompletableFuture getIdentityAsync(String authorizationHeader, String channelId) { + /** + * Get a ClaimsIdentity from an auth header and channel id. + * + * @param authorizationHeader The Authorization header value. + * @param channelId The channel id. + * @param requiredEndorsements A list of endorsements that are required. + * @return A ClaimsIdentity if successful. + */ + public CompletableFuture getIdentity( + String authorizationHeader, + String channelId, + List requiredEndorsements + ) { if (authorizationHeader == null) { return CompletableFuture.completedFuture(null); } String[] parts = authorizationHeader.split(" "); if (parts.length == 2) { - return getIdentityAsync(parts[0], parts[1], channelId); + return getIdentity(parts[0], parts[1], channelId, requiredEndorsements); } return CompletableFuture.completedFuture(null); } - public CompletableFuture getIdentityAsync(String schema, String token, String channelId) { + /** + * Get a ClaimsIdentity from a schema, token and channel id. + * + * @param schema The schema. + * @param token The token. + * @param channelId The channel id. + * @param requiredEndorsements A list of endorsements that are required. + * @return A ClaimsIdentity if successful. + */ + public CompletableFuture getIdentity( + String schema, + String token, + String channelId, + List requiredEndorsements + ) { // No header in correct scheme or no token if (!schema.equalsIgnoreCase("bearer") || token == null) { return CompletableFuture.completedFuture(null); } // Issuer isn't allowed? No need to check signature - if (!this.hasAllowedIssuer(token)) { + if (!hasAllowedIssuer(token)) { return CompletableFuture.completedFuture(null); } - return this.validateTokenAsync(token, channelId); + return validateToken(token, channelId, requiredEndorsements); } private boolean hasAllowedIssuer(String token) { DecodedJWT decodedJWT = JWT.decode(token); - return this.tokenValidationParameters.validIssuers != null && this.tokenValidationParameters.validIssuers.contains(decodedJWT.getIssuer()); + return this.tokenValidationParameters.validIssuers != null + && this.tokenValidationParameters.validIssuers.contains(decodedJWT.getIssuer()); } @SuppressWarnings("unchecked") - private CompletableFuture validateTokenAsync(String token, String channelId) { - DecodedJWT decodedJWT = JWT.decode(token); - OpenIdMetadataKey key = this.openIdMetadata.getKey(decodedJWT.getKeyId()); + private CompletableFuture validateToken( + String token, + String channelId, + List requiredEndorsements + ) { + return CompletableFuture.supplyAsync(() -> { + DecodedJWT decodedJWT = JWT.decode(token); + OpenIdMetadataKey key = this.openIdMetadata.getKey(decodedJWT.getKeyId()); + if (key == null) { + return null; + } - if (key != null) { - Verification verification = JWT.require(Algorithm.RSA256(key.key, null)); + Verification verification = JWT.require(Algorithm.RSA256(key.key, null)) + .acceptLeeway(tokenValidationParameters.clockSkew.getSeconds()); try { verification.build().verify(token); - // Note: On the Emulator Code Path, the endorsements collection is null so the validation code - // below won't run. This is normal. + // If specified, validate the signing certificate. + if ( + tokenValidationParameters.validateIssuerSigningKey + && key.certificateChain != null + && key.certificateChain.size() > 0 + ) { + X509Certificate cert = decodeCertificate(key.certificateChain.get(0)); + if (!isCertValid(cert)) { + throw new JWTVerificationException("Signing certificate is not valid"); + } + } + + // Note: On the Emulator Code Path, the endorsements collection is null so the + // validation code below won't run. This is normal. if (key.endorsements != null) { - // Validate Channel / Token Endorsements. For this, the channelID present on the Activity - // needs to be matched by an endorsement. - boolean isEndorsed = EndorsementsValidator.validate(channelId, key.endorsements); + // Validate Channel / Token Endorsements. For this, the channelID present on the + // Activity needs to be matched by an endorsement. + boolean isEndorsed = + EndorsementsValidator.validate(channelId, key.endorsements); if (!isEndorsed) { - throw new AuthenticationException(String.format("Could not validate endorsement for key: %s with endorsements: %s", key.key.toString(), StringUtils.join(key.endorsements))); + throw new AuthenticationException( + String.format( + "Could not validate endorsement for key: %s with endorsements: %s", + key.key.toString(), StringUtils.join(key.endorsements) + ) + ); + } + + // Verify that additional endorsements are satisfied. If no additional + // endorsements are expected, the requirement is satisfied as well + boolean additionalEndorsementsSatisfied = requiredEndorsements.stream() + .allMatch( + (endorsement) -> EndorsementsValidator + .validate(endorsement, key.endorsements) + ); + if (!additionalEndorsementsSatisfied) { + throw new AuthenticationException( + String.format( + "Could not validate additional endorsement for key: %s with endorsements: %s", + key.key.toString(), StringUtils.join(requiredEndorsements) + ) + ); } } if (!this.allowedSigningAlgorithms.contains(decodedJWT.getAlgorithm())) { - throw new AuthenticationException(String.format("Could not validate algorithm for key: %s with algorithms: %s", decodedJWT.getAlgorithm(), StringUtils.join(allowedSigningAlgorithms))); + throw new AuthenticationException( + String.format( + "Could not validate algorithm for key: %s with algorithms: %s", + decodedJWT.getAlgorithm(), StringUtils.join(allowedSigningAlgorithms) + ) + ); } - Map claims = new HashMap<>(); - if (decodedJWT.getClaims() != null) { - decodedJWT.getClaims().forEach((k, v) -> claims.put(k, v.asString())); - } + return new ClaimsIdentity(decodedJWT); + } catch (JWTVerificationException | CertificateException ex) { + LOGGER.warn(ex.getMessage()); + throw new AuthenticationException(ex); + } + }, ExecutorFactory.getExecutor()); + } - return CompletableFuture.completedFuture(new ClaimsIdentityImpl(decodedJWT.getIssuer(), claims)); + private X509Certificate decodeCertificate(String certStr) throws CertificateException { + byte[] decoded = Base64.getDecoder().decode(certStr); + return (X509Certificate) CertificateFactory + .getInstance("X.509").generateCertificate(new ByteArrayInputStream(decoded)); + } - } catch (JWTVerificationException ex) { - String errorDescription = ex.getMessage(); - LOGGER.log(Level.WARNING, errorDescription); - return CompletableFuture.completedFuture(null); - } + private boolean isCertValid(X509Certificate cert) { + if (cert == null) { + return false; } - return CompletableFuture.completedFuture(null); + long now = new Date().getTime(); + long clockskew = tokenValidationParameters.clockSkew.toMillis(); + long startValid = cert.getNotBefore().getTime() - clockskew; + long endValid = cert.getNotAfter().getTime() + clockskew; + return now >= startValid && now <= endValid; } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java index f19c5c36b..26639614c 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java @@ -3,66 +3,277 @@ package com.microsoft.bot.connector.authentication; -import com.auth0.jwt.interfaces.Claim; -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.connector.authentication.ClaimsIdentityImpl; -import com.microsoft.bot.connector.authentication.EmulatorValidation; -import com.microsoft.bot.schema.models.Activity; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.RoleTypes; + +import java.util.Map; +import org.apache.commons.lang3.StringUtils; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -public class JwtTokenValidation { +/** + * Contains helper methods for authenticating incoming HTTP requests. + */ +public final class JwtTokenValidation { + private JwtTokenValidation() { + + } /** - * Validates the security tokens required by the Bot Framework Protocol. Throws on any exceptions. + * Authenticates the request and add's the activity's + * {@link Activity#getServiceUrl()} to the set of trusted URLs. * - * @param activity The incoming Activity from the Bot Framework or the Emulator - * @param authHeader The Bearer token included as part of the request - * @param credentials The set of valid credentials, such as the Bot Application ID - * @return Nothing + * @param activity The incoming Activity from the Bot Framework or the + * Emulator + * @param authHeader The Bearer token included as part of the request + * @param credentials The bot's credential provider. + * @param channelProvider The bot's channel service provider. + * @return A task that represents the work queued to execute. * @throws AuthenticationException Throws on auth failed. */ - public static CompletableFuture authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials) throws AuthenticationException, InterruptedException, ExecutionException { - if (authHeader == null || authHeader.isEmpty()) { + public static CompletableFuture authenticateRequest( + Activity activity, + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider + ) { + return authenticateRequest( + activity, authHeader, credentials, channelProvider, new AuthenticationConfiguration() + ); + } + + /** + * Authenticates the request and add's the activity's + * {@link Activity#getServiceUrl()} to the set of trusted URLs. + * + * @param activity The incoming Activity from the Bot Framework or the + * Emulator + * @param authHeader The Bearer token included as part of the request + * @param credentials The bot's credential provider. + * @param channelProvider The bot's channel service provider. + * @param authConfig The optional authentication configuration. + * @return A task that represents the work queued to execute. + * @throws AuthenticationException Throws on auth failed. + */ + public static CompletableFuture authenticateRequest( + Activity activity, + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + AuthenticationConfiguration authConfig + ) { + if (authConfig == null) { + return Async.completeExceptionally( + new IllegalArgumentException("authConfig cannot be null") + ); + } + + if (StringUtils.isBlank(authHeader)) { // No auth header was sent. We might be on the anonymous code path. - boolean isAuthDisable = credentials.isAuthenticationDisabledAsync().get(); - if (isAuthDisable) { + return credentials.isAuthenticationDisabled().thenApply(isAuthDisable -> { + if (!isAuthDisable) { + // No Auth Header. Auth is required. Request is not authorized. + throw new AuthenticationException("No Auth Header. Auth is required."); + } + + if (activity.getChannelId() != null + && activity.getChannelId().equals(Channels.EMULATOR) + && activity.getRecipient() != null + && activity.getRecipient().getRole().equals(RoleTypes.SKILL)) { + return SkillValidation.createAnonymousSkillClaim(); + } + // In the scenario where Auth is disabled, we still want to have the // IsAuthenticated flag set in the ClaimsIdentity. To do this requires // adding in an empty claim. - return CompletableFuture.completedFuture(new ClaimsIdentityImpl("anonymous")); - } - - // No Auth Header. Auth is required. Request is not authorized. - throw new AuthenticationException("No Auth Header. Auth is required."); + return new ClaimsIdentity(AuthenticationConstants.ANONYMOUS_AUTH_TYPE); + }); } - // Go through the standard authentication path. - ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(authHeader, credentials, activity.channelId(), activity.serviceUrl()).get(); + // Go through the standard authentication path. This will throw + // AuthenticationException if + // it fails. + return JwtTokenValidation.validateAuthHeader( + authHeader, credentials, channelProvider, activity.getChannelId(), + activity.getServiceUrl(), authConfig + ); + } - // On the standard Auth path, we need to trust the URL that was incoming. - MicrosoftAppCredentials.trustServiceUrl(activity.serviceUrl()); - return CompletableFuture.completedFuture(identity); + /** + * Validates the authentication header of an incoming request. + * + * @param authHeader The authentication header to validate. + * @param credentials The bot's credential provider. + * @param channelProvider The bot's channel service provider. + * @param channelId The ID of the channel that sent the request. + * @param serviceUrl The service URL for the activity. + * @return A task that represents the work queued to execute. + * + * On Call: + * @throws IllegalArgumentException Incorrect arguments supplied + * + * On join: + * @throws AuthenticationException Authentication Error + */ + public static CompletableFuture validateAuthHeader( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String channelId, + String serviceUrl + ) { + return validateAuthHeader( + authHeader, credentials, channelProvider, channelId, serviceUrl, + new AuthenticationConfiguration() + ); } - // TODO: Recieve httpClient and use ClientID - public static CompletableFuture validateAuthHeader(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl) throws ExecutionException, InterruptedException, AuthenticationException { - if (authHeader == null || authHeader.isEmpty()) { - throw new IllegalArgumentException("No authHeader present. Auth is required."); + /** + * Validates the authentication header of an incoming request. + * + * @param authHeader The authentication header to validate. + * @param credentials The bot's credential provider. + * @param channelProvider The bot's channel service provider. + * @param channelId The ID of the channel that sent the request. + * @param serviceUrl The service URL for the activity. + * @param authConfig The authentication configuration. + * @return A task that represents the work queued to execute. + * @throws IllegalArgumentException Incorrect arguments supplied + * @throws AuthenticationException Authentication Error + */ + public static CompletableFuture validateAuthHeader( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String channelId, + String serviceUrl, + AuthenticationConfiguration authConfig + ) { + if (StringUtils.isEmpty(authHeader)) { + return Async.completeExceptionally( + new IllegalArgumentException("No authHeader present. Auth is required.")); } - boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader).get(); + return authenticateToken(authHeader, credentials, channelProvider, channelId, serviceUrl, authConfig) + .thenApply(identity -> { + validateClaims(authConfig, identity.claims()); + return identity; + } + ); + } + + private static CompletableFuture validateClaims( + AuthenticationConfiguration authConfig, + Map claims + ) { + if (authConfig.getClaimsValidator() != null) { + return authConfig.getClaimsValidator().validateClaims(claims); + } else if (SkillValidation.isSkillClaim(claims)) { + return Async.completeExceptionally( + new RuntimeException("ClaimValidator is required for validation of Skill Host calls") + ); + } + return CompletableFuture.completedFuture(null); + } + + private static CompletableFuture authenticateToken( + String authHeader, + CredentialProvider credentials, + ChannelProvider channelProvider, + String channelId, + String serviceUrl, + AuthenticationConfiguration authConfig + ) { + if (SkillValidation.isSkillToken(authHeader)) { + return SkillValidation.authenticateChannelToken( + authHeader, credentials, channelProvider, channelId, authConfig); + } + boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader); if (usingEmulator) { - return EmulatorValidation.authenticateToken(authHeader, credentials, channelId); - } else { + return EmulatorValidation + .authenticateToken(authHeader, credentials, channelProvider, channelId, authConfig); + } else if (channelProvider == null || channelProvider.isPublicAzure()) { // No empty or null check. Empty can point to issues. Null checks only. if (serviceUrl != null) { - return ChannelValidation.authenticateToken(authHeader, credentials, channelId, serviceUrl); + return ChannelValidation + .authenticateToken(authHeader, credentials, channelId, serviceUrl, authConfig); } else { - return ChannelValidation.authenticateToken(authHeader, credentials, channelId); + return ChannelValidation + .authenticateToken(authHeader, credentials, channelId, authConfig); } + } else if (channelProvider.isGovernment()) { + return GovernmentChannelValidation + .authenticateToken(authHeader, credentials, serviceUrl, channelId, authConfig); + } else { + return EnterpriseChannelValidation.authenticateToken( + authHeader, credentials, channelProvider, serviceUrl, channelId, authConfig + ); + } + + } + + /** + * Gets the AppId from claims. + * + *

+ * In v1 tokens the AppId is in the the AppIdClaim claim. In v2 tokens the AppId + * is in the AuthorizedParty claim. + *

+ * + * @param claims The map of claims. + * @return The value of the appId claim if found (null if it can't find a + * suitable claim). + * @throws IllegalArgumentException Missing claims + */ + public static String getAppIdFromClaims(Map claims) throws IllegalArgumentException { + if (claims == null) { + throw new IllegalArgumentException("claims"); + } + + String appId = null; + + String tokenVersion = claims.get(AuthenticationConstants.VERSION_CLAIM); + if (StringUtils.isEmpty(tokenVersion) || tokenVersion.equalsIgnoreCase("1.0")) { + // either no Version or a version of "1.0" means we should look for the claim in + // the "appid" claim. + appId = claims.get(AuthenticationConstants.APPID_CLAIM); + } else { + // "2.0" puts the AppId in the "azp" claim. + appId = claims.get(AuthenticationConstants.AUTHORIZED_PARTY); + } + + return appId; + } + + /** + * Internal helper to check if the token has the shape we expect "Bearer [big long string]". + * + * @param authHeader A string containing the token header. + * @return True if the token is valid, false if not. + */ + public static boolean isValidTokenFormat(String authHeader) { + if (StringUtils.isEmpty(authHeader)) { + // No token, not valid. + return false; + } + + String[] parts = authHeader.split(" "); + if (parts.length != 2) { + // Tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not a valid token + return false; } + + // We now have an array that should be: + // [0] = "Bearer" + // [1] = "[Big Long String]" + String authScheme = parts[0]; + if (!StringUtils.equals(authScheme, "Bearer")) { + // The scheme MUST be "Bearer" + return false; + } + + return true; } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java index bfc627315..801b9c04e 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java @@ -3,151 +3,108 @@ package com.microsoft.bot.connector.authentication; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.rest.credentials.ServiceClientCredentials; -import okhttp3.*; - -import java.io.IOException; import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToChannelFromBotLoginUrl; -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToChannelFromBotOAuthScope; - -public class MicrosoftAppCredentials implements ServiceClientCredentials { - private String appId; - private String appPassword; - - private OkHttpClient client; - private ObjectMapper mapper; - public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - public static final MediaType FORM_ENCODE = MediaType.parse("application/x-www-form-urlencoded"); - - private String currentToken = null; - private long expiredTime = 0; - private static final Object cacheSync = new Object(); - protected static final HashMap cache = new HashMap(); - public final String OAuthEndpoint = AuthenticationConstants.ToChannelFromBotLoginUrl; - public final String OAuthScope = AuthenticationConstants.ToChannelFromBotOAuthScope; - - - public String getTokenCacheKey() { - return String.format("%s-cache", this.appId); - } - - public MicrosoftAppCredentials(String appId, String appPassword) { - this.appId = appId; - this.appPassword = appPassword; - this.client = new OkHttpClient.Builder().build(); - this.mapper = new ObjectMapper().findAndRegisterModules(); - } - - public static final MicrosoftAppCredentials Empty = new MicrosoftAppCredentials(null, null); - - public String microsoftAppId() { - return this.appId; - } - - public MicrosoftAppCredentials withMicrosoftAppId(String appId) { - this.appId = appId; - return this; - } - - public String getToken(Request request) throws IOException { - if (System.currentTimeMillis() < expiredTime) { - return currentToken; - } - Request reqToken = request.newBuilder() - .url(ToChannelFromBotLoginUrl) - .post(new FormBody.Builder() - .add("grant_type", "client_credentials") - .add("client_id", this.appId) - .add("client_secret", this.appPassword) - .add("scope", ToChannelFromBotOAuthScope) - .build()).build(); - Response response = this.client.newCall(reqToken).execute(); - if (response.isSuccessful()) { - String payload = response.body().string(); - AuthenticationResponse authResponse = this.mapper.readValue(payload, AuthenticationResponse.class); - this.expiredTime = System.currentTimeMillis() + (authResponse.expiresIn * 1000); - this.currentToken = authResponse.accessToken; - } - return this.currentToken; - } - - - private boolean ShouldSetToken(String url) { - if (isTrustedServiceUrl(url)) { - return true; - } - return false; - } +/** + * MicrosoftAppCredentials auth implementation and cache. + */ +public class MicrosoftAppCredentials extends AppCredentials { + /** + * The configuration property for the Microsoft app Password. + */ + public static final String MICROSOFTAPPID = "MicrosoftAppId"; + /** + * The configuration property for the Microsoft app ID. + */ + public static final String MICROSOFTAPPPASSWORD = "MicrosoftAppPassword"; - @Override - public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { - clientBuilder.interceptors().add(new MicrosoftAppCredentialsInterceptor(this)); - } - - private static class AuthenticationResponse { - @JsonProperty(value = "token_type") - String tokenType; - @JsonProperty(value = "expires_in") - long expiresIn; - @JsonProperty(value = "ext_expires_in") - long extExpiresIn; - @JsonProperty(value = "access_token") - String accessToken; - } - - - public static void trustServiceUrl(URI serviceUrl) { - trustServiceUrl(serviceUrl.toString(), LocalDateTime.now().plusDays(1)); - } - - public static void trustServiceUrl(String serviceUrl) { - trustServiceUrl(serviceUrl, LocalDateTime.now().plusDays(1)); - } - - public static void trustServiceUrl(String serviceUrl, LocalDateTime expirationTime) { - try { - URL url = new URL(serviceUrl); - trustServiceUrl(url, expirationTime); - } catch (MalformedURLException e) { - } - } - - public static void trustServiceUrl(URL serviceUrl, LocalDateTime expirationTime) { - trustHostNames.put(serviceUrl.getHost(), expirationTime); - } - - public static boolean isTrustedServiceUrl(String serviceUrl) { - try { - URL url = new URL(serviceUrl); - return isTrustedServiceUrl(url); - } catch (MalformedURLException e) { - return false; - } - } - - public static boolean isTrustedServiceUrl(URL url) { - return !trustHostNames.getOrDefault(url.getHost(), LocalDateTime.MIN).isBefore(LocalDateTime.now().minusMinutes(5)); - } - - public static boolean isTrustedServiceUrl(HttpUrl url) { - return !trustHostNames.getOrDefault(url.host(), LocalDateTime.MIN).isBefore(LocalDateTime.now().minusMinutes(5)); - } - - private static ConcurrentMap trustHostNames = new ConcurrentHashMap<>(); + private String appPassword; - static { - trustHostNames.put("state.botframework.com", LocalDateTime.MAX); + /** + * Returns an empty set of credentials. + * + * @return A empty set of MicrosoftAppCredentials. + */ + public static MicrosoftAppCredentials empty() { + return new MicrosoftAppCredentials(null, null); + } + + /** + * Initializes a new instance of the MicrosoftAppCredentials class. + * + * @param withAppId The Microsoft app ID. + * @param withAppPassword The Microsoft app password. + */ + public MicrosoftAppCredentials(String withAppId, String withAppPassword) { + this(withAppId, withAppPassword, null); + } + + /** + * Initializes a new instance of the MicrosoftAppCredentials class. + * + * @param withAppId The Microsoft app ID. + * @param withAppPassword The Microsoft app password. + * @param withChannelAuthTenant Optional. The oauth token tenant. + */ + public MicrosoftAppCredentials( + String withAppId, + String withAppPassword, + String withChannelAuthTenant + ) { + super(withChannelAuthTenant); + setAppId(withAppId); + setAppPassword(withAppPassword); + } + + /** + * Initializes a new instance of the MicrosoftAppCredentials class. + * + * @param withAppId The Microsoft app ID. + * @param withAppPassword The Microsoft app password. + * @param withChannelAuthTenant Optional. The oauth token tenant. + * @param withOAuthScope The scope for the token. + */ + public MicrosoftAppCredentials( + String withAppId, + String withAppPassword, + String withChannelAuthTenant, + String withOAuthScope + ) { + super(withChannelAuthTenant, withOAuthScope); + setAppId(withAppId); + setAppPassword(withAppPassword); + } + + /** + * Gets the app password for this credential. + * + * @return The app password. + */ + public String getAppPassword() { + return appPassword; + } + + /** + * Sets the app password for this credential. + * + * @param withAppPassword The app password. + */ + public void setAppPassword(String withAppPassword) { + appPassword = withAppPassword; + } + + /** + * Returns an credentials Authenticator. + * + * @return A CredentialsAuthenticator. + * @throws MalformedURLException Invalid endpoint url. + */ + protected Authenticator buildAuthenticator() throws MalformedURLException { + return new CredentialsAuthenticator( + getAppId(), + getAppPassword(), + new OAuthConfiguration(oAuthEndpoint(), oAuthScope()) + ); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java deleted file mode 100644 index 48d3f0d3e..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentialsInterceptor.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.bot.connector.authentication; - -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -import java.io.IOException; - -/** - * Token credentials filter for placing a token credential into request headers. - */ -class MicrosoftAppCredentialsInterceptor implements Interceptor { - /** - * The credentials instance to apply to the HTTP client pipeline. - */ - private MicrosoftAppCredentials credentials; - - /** - * Initialize a TokenCredentialsFilter class with a - * TokenCredentials credential. - * - * @param credentials a TokenCredentials instance - */ - MicrosoftAppCredentialsInterceptor(MicrosoftAppCredentials credentials) { - this.credentials = credentials; - } - - @Override - public Response intercept(Chain chain) throws IOException { - if (MicrosoftAppCredentials.isTrustedServiceUrl(chain.request().url().url().toString())) { - Request newRequest = chain.request().newBuilder() - .header("Authorization", "Bearer " + this.credentials.getToken(chain.request())) - .build(); - return chain.proceed(newRequest); - } - return chain.proceed(chain.request()); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java new file mode 100644 index 000000000..88190f19b --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import org.apache.commons.lang3.StringUtils; + +/** + * MicrosoftGovernmentAppCredentials auth implementation. + */ +public class MicrosoftGovernmentAppCredentials extends MicrosoftAppCredentials { + /** + * Initializes a new instance of the MicrosoftGovernmentAppCredentials class. + * + * @param appId The Microsoft app ID. + * @param password The Microsoft app password. + */ + public MicrosoftGovernmentAppCredentials(String appId, String password) { + super( + appId, + password, + null, + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ); + } + + /** + * Initializes a new instance of the MicrosoftGovernmentAppCredentials class. + * + * @param appId The Microsoft app ID. + * @param password The Microsoft app password. + * @param oAuthScope The scope for the token. + */ + public MicrosoftGovernmentAppCredentials(String appId, String password, String oAuthScope) { + super( + appId, + password, + null, + StringUtils.isEmpty(oAuthScope) + ? GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + : oAuthScope + ); + } + + /** + * Initializes a new instance of the MicrosoftGovernmentAppCredentials class. + * + * @param withAppId The Microsoft app ID. + * @param withAppPassword The Microsoft app password. + * @param withChannelAuthTenant Optional. The oauth token tenant. + * @param withOAuthScope The scope for the token. + */ + public MicrosoftGovernmentAppCredentials( + String withAppId, + String withAppPassword, + String withChannelAuthTenant, + String withOAuthScope + ) { + super(withAppId, withAppPassword, withChannelAuthTenant, withOAuthScope); + } + + /** + * An empty set of credentials. + * + * @return An empty Gov credentials. + */ + public static MicrosoftGovernmentAppCredentials empty() { + return new MicrosoftGovernmentAppCredentials(null, null); + } + + /** + * Gets the Gov OAuth endpoint to use. + * + * @return The OAuth endpoint to use. + */ + @Override + public String oAuthEndpoint() { + return GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java deleted file mode 100644 index 902a21221..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthClient.java +++ /dev/null @@ -1,343 +0,0 @@ -package com.microsoft.bot.connector.authentication; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.bot.connector.UserAgent; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.TokenExchangeState; -import com.microsoft.bot.schema.models.Activity; -import com.microsoft.bot.schema.models.ConversationReference; -import com.microsoft.bot.schema.models.TokenResponse; -import com.microsoft.rest.ServiceClient; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import static com.microsoft.bot.connector.authentication.MicrosoftAppCredentials.JSON; -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static java.net.HttpURLConnection.HTTP_OK; -import static java.util.stream.Collectors.joining; - - -/** - * Service client to handle requests to the botframework api service. - *

- * Uses the MicrosoftInterceptor class to add Authorization header from idp. - */ -public class OAuthClient extends ServiceClient { - private final ConnectorClientImpl client; - private final String uri; - - private ObjectMapper mapper; - - - public OAuthClient(ConnectorClientImpl client, String uri) throws URISyntaxException, MalformedURLException { - super(client.restClient()); - URI uriResult = new URI(uri); - - // Sanity check our url - uriResult.toURL(); - String scheme = uriResult.getScheme(); - if (!scheme.toLowerCase().equals("https")) - throw new IllegalArgumentException("Please supply a valid https uri"); - if (client == null) - throw new IllegalArgumentException("client"); - this.client = client; - this.uri = uri + (uri.endsWith("/") ? "" : "/"); - this.mapper = new ObjectMapper(); - } - - /** - * Get User Token for given user and connection. - * - * @param userId - * @param connectionName - * @param magicCode - * @return CompletableFuture< TokenResponse > on success; otherwise null. - */ - public CompletableFuture GetUserTokenAsync(String userId, String connectionName, String magicCode) throws IOException, URISyntaxException, ExecutionException, InterruptedException { - return GetUserTokenAsync(userId, connectionName, magicCode, null); - } - - protected URI MakeUri(String uri, HashMap queryStrings) throws URISyntaxException { - String newUri = queryStrings.keySet().stream() - .map(key -> { - try { - return key + "=" + URLEncoder.encode(queryStrings.get(key), StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - }) - .collect(joining("&", (uri.endsWith("?") ? uri : uri + "?"), "")); - return new URI(newUri); - - - } - - /** - * Get User Token for given user and connection. - * - * @param userId - * @param connectionName - * @param magicCode - * @param customHeaders - * @return CompletableFuture< TokenResponse > on success; null otherwise. - */ - public CompletableFuture GetUserTokenAsync(String userId, String connectionName, String magicCode, Map> customHeaders) throws IllegalArgumentException { - if (StringUtils.isEmpty(userId)) { - throw new IllegalArgumentException("userId"); - } - if (StringUtils.isEmpty(connectionName)) { - throw new IllegalArgumentException("connectionName"); - } - - return CompletableFuture.supplyAsync(() -> { - // Construct URL - HashMap qstrings = new HashMap<>(); - qstrings.put("userId", userId); - qstrings.put("connectionName", connectionName); - if (!StringUtils.isBlank(magicCode)) { - qstrings.put("code", magicCode); - } - String strUri = String.format("%sapi/usertoken/GetToken", this.uri); - URI tokenUrl = null; - try { - tokenUrl = MakeUri(strUri, qstrings); - } catch (URISyntaxException e) { - e.printStackTrace(); - return null; - } - - // add botframework api service url to the list of trusted service url's for these app credentials. - MicrosoftAppCredentials.trustServiceUrl(tokenUrl.toString()); - - // Set Credentials and make call - MicrosoftAppCredentials appCredentials = (MicrosoftAppCredentials) client.restClient().credentials(); - - // Later: Use client in clientimpl? - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) - .build(); - - Request request = new Request.Builder() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - int statusCode = response.code(); - if (statusCode == HTTP_OK) { - return this.mapper.readValue(response.body().string(), TokenResponse.class); - } else if (statusCode == HTTP_NOT_FOUND) { - return null; - } else { - return null; - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (response != null) - response.body().close(); - } - return null; - }); - } - - /** - * Signs Out the User for the given ConnectionName. - * - * @param userId - * @param connectionName - * @return True on successful sign-out; False otherwise. - */ - public CompletableFuture SignOutUserAsync(String userId, String connectionName) throws URISyntaxException, IOException { - if (StringUtils.isEmpty(userId)) { - throw new IllegalArgumentException("userId"); - } - if (StringUtils.isEmpty(connectionName)) { - throw new IllegalArgumentException("connectionName"); - } - - return CompletableFuture.supplyAsync(() -> { - String invocationId = null; - - // Construct URL - HashMap qstrings = new HashMap<>(); - qstrings.put("userId", userId); - qstrings.put("connectionName", connectionName); - String strUri = String.format("%sapi/usertoken/SignOut", this.uri); - URI tokenUrl = null; - try { - tokenUrl = MakeUri(strUri, qstrings); - } catch (URISyntaxException e) { - e.printStackTrace(); - return false; - } - - // add botframework api service url to the list of trusted service url's for these app credentials. - MicrosoftAppCredentials.trustServiceUrl(tokenUrl); - - // Set Credentials and make call - MicrosoftAppCredentials appCredentials = (MicrosoftAppCredentials) client.restClient().credentials(); - - // Later: Use client in clientimpl? - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) - .build(); - - Request request = new Request.Builder() - .delete() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - int statusCode = response.code(); - if (statusCode == HTTP_OK) - return true; - } catch (IOException e) { - e.printStackTrace(); - } - return false; - - }); - } - - - /** - * Gets the Link to be sent to the user for signin into the given ConnectionName - * - * @param activity - * @param connectionName - * @return Sign in link on success; null otherwise. - */ - public CompletableFuture GetSignInLinkAsync(Activity activity, String connectionName) throws IllegalArgumentException, URISyntaxException, JsonProcessingException { - if (StringUtils.isEmpty(connectionName)) { - throw new IllegalArgumentException("connectionName"); - } - if (activity == null) { - throw new IllegalArgumentException("activity"); - } - final MicrosoftAppCredentials creds = (MicrosoftAppCredentials) this.client.restClient().credentials(); - TokenExchangeState tokenExchangeState = new TokenExchangeState() - .withConnectionName(connectionName) - .withConversation(new ConversationReference() - .withActivityId(activity.id()) - .withBot(activity.recipient()) - .withChannelId(activity.channelId()) - .withConversation(activity.conversation()) - .withServiceUrl(activity.serviceUrl()) - .withUser(activity.from())) - .withMsAppId((creds == null) ? null : creds.microsoftAppId()); - - String serializedState = this.mapper.writeValueAsString(tokenExchangeState); - - // Construct URL - String encoded = Base64.getEncoder().encodeToString(serializedState.getBytes(StandardCharsets.UTF_8)); - HashMap qstrings = new HashMap<>(); - qstrings.put("state", encoded); - - String strUri = String.format("%sapi/botsignin/getsigninurl", this.uri); - final URI tokenUrl = MakeUri(strUri, qstrings); - - return CompletableFuture.supplyAsync(() -> { - - // add botframework api service url to the list of trusted service url's for these app credentials. - MicrosoftAppCredentials.trustServiceUrl(tokenUrl); - - - // Later: Use client in clientimpl? - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(creds)) - .build(); - - Request request = new Request.Builder() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - int statusCode = response.code(); - if (statusCode == HTTP_OK) - return response.body().string(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - }); - } - - /** - * Send a dummy OAuth card when the bot is being used on the emulator for testing without fetching a real token. - * - * @param emulateOAuthCards - * @return CompletableFuture with no result code - */ - public CompletableFuture SendEmulateOAuthCardsAsync(Boolean emulateOAuthCards) throws URISyntaxException, IOException { - - // Construct URL - HashMap qstrings = new HashMap<>(); - qstrings.put("emulate", emulateOAuthCards.toString()); - String strUri = String.format("%sapi/usertoken/emulateOAuthCards", this.uri); - URI tokenUrl = MakeUri(strUri, qstrings); - - // add botframework api service url to the list of trusted service url's for these app credentials. - MicrosoftAppCredentials.trustServiceUrl(tokenUrl); - - return CompletableFuture.runAsync(() -> { - // Construct dummy body - RequestBody body = RequestBody.create(JSON, "{}"); - - // Set Credentials and make call - MicrosoftAppCredentials appCredentials = (MicrosoftAppCredentials) client.restClient().credentials(); - - // Later: Use client in clientimpl? - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials)) - .build(); - - Request request = new Request.Builder() - .url(tokenUrl.toString()) - .header("User-Agent", UserAgent.value()) - .post(body) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - int statusCode = response.code(); - if (statusCode == HTTP_OK) - return; - } catch (IOException e) { - e.printStackTrace(); - } - - - // Apparently swallow any results - return; - - }); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java new file mode 100644 index 000000000..9376c4865 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +/** + * Configuration for OAuth client credential authentication. + */ +public class OAuthConfiguration { + private String scope; + private String authority; + + /** + * Construct with authority and scope. + * + * @param withAuthority The auth authority. + * @param withScope The auth scope. + */ + public OAuthConfiguration(String withAuthority, String withScope) { + this.authority = withAuthority; + this.scope = withScope; + } + + /** + * Sets oAuth Authority for authentication. + * + * @param withAuthority oAuth Authority for authentication. + */ + public void setAuthority(String withAuthority) { + authority = withAuthority; + } + + /** + * Gets oAuth Authority for authentication. + * + * @return OAuth Authority for authentication. + */ + public String getAuthority() { + return authority; + } + + /** + * Sets oAuth scope for authentication. + * + * @param withScope oAuth Authority for authentication. + */ + public void setScope(String withScope) { + scope = withScope; + } + + /** + * Gets oAuth scope for authentication. + * + * @return OAuth scope for authentication. + */ + public String getScope() { + return scope; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthResponse.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthResponse.java deleted file mode 100644 index 7b589f57a..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.microsoft.bot.connector.authentication; - - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.joda.time.DateTime; - -import java.util.HashMap; - -/** - * - * Member variables to this class follow the RFC Naming conventions - * "properties" house any "extra" properties that aren't used at the moment. - * - */ - -public class OAuthResponse -{ - @JsonProperty - private String token_type; - public String getTokenType() { - return this.token_type; - } - @JsonProperty - private int expires_in; - public int getExpiresIn() { - return this.expires_in; - } - @JsonProperty - private String access_token; - public String getAccessToken() { - return this.access_token; - } - @JsonProperty - private DateTime expiration_time; - public DateTime getExpirationTime() { - return this.expiration_time; - } - public OAuthResponse withExpirationTime(DateTime expirationTime) { - this.expiration_time = expirationTime; - return this; - } - - @JsonAnySetter - private HashMap properties; - -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java index 45378175f..6af6a8359 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java @@ -3,72 +3,15 @@ package com.microsoft.bot.connector.authentication; -import com.auth0.jwk.Jwk; -import com.auth0.jwk.JwkException; -import com.auth0.jwk.JwkProvider; -import com.auth0.jwk.UrlJwkProvider; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.net.URL; -import java.security.interfaces.RSAPublicKey; -import java.util.HashMap; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -class OpenIdMetadata { - private static final Logger LOGGER = Logger.getLogger( OpenIdMetadata.class.getName() ); - - private String url; - private long lastUpdated; - private JwkProvider cacheKeys; - private ObjectMapper mapper; - - OpenIdMetadata(String url) { - this.url = url; - this.mapper = new ObjectMapper().findAndRegisterModules(); - } - - public OpenIdMetadataKey getKey(String keyId) { - // If keys are more than 5 days old, refresh them - long now = System.currentTimeMillis(); - if (this.lastUpdated < (now - (1000 * 60 * 60 * 24 * 5))) { - refreshCache(); - } - // Search the cache even if we failed to refresh - return findKey(keyId); - } - - private String refreshCache() { - try { - URL openIdUrl = new URL(this.url); - HashMap openIdConf = this.mapper.readValue(openIdUrl, new TypeReference>(){}); - URL keysUrl = new URL(openIdConf.get("jwks_uri")); - this.lastUpdated = System.currentTimeMillis(); - this.cacheKeys = new UrlJwkProvider(keysUrl); - return IOUtils.toString(keysUrl); - } catch (IOException e) { - String errorDescription = String.format("Failed to load openID config: %s", e.getMessage()); - LOGGER.log(Level.WARNING, errorDescription); - } - return null; - } - - @SuppressWarnings("unchecked") - private OpenIdMetadataKey findKey(String keyId) { - try { - Jwk jwk = this.cacheKeys.get(keyId); - OpenIdMetadataKey key = new OpenIdMetadataKey(); - key.key = (RSAPublicKey) jwk.getPublicKey(); - key.endorsements = (List) jwk.getAdditionalAttributes().get("endorsements"); - return key; - } catch (JwkException e) { - String errorDescription = String.format("Failed to load keys: %s", e.getMessage()); - LOGGER.log(Level.WARNING, errorDescription); - } - return null; - } +/** + * Fetches Jwk data. + */ +public interface OpenIdMetadata { + + /** + * Returns the partial Jwk data for a key. + * @param keyId The key id. + * @return The Jwk data. + */ + OpenIdMetadataKey getKey(String keyId); } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java index 94982d5dc..20c1774d9 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java @@ -6,7 +6,14 @@ import java.security.interfaces.RSAPublicKey; import java.util.List; -class OpenIdMetadataKey { - RSAPublicKey key; - List endorsements; +/** + * Wrapper to hold Jwk key data. + */ +public class OpenIdMetadataKey { + @SuppressWarnings("checkstyle:VisibilityModifier") + public RSAPublicKey key; + @SuppressWarnings("checkstyle:VisibilityModifier") + public List endorsements; + @SuppressWarnings("checkstyle:VisibilityModifier") + public List certificateChain; } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataResolver.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataResolver.java new file mode 100644 index 000000000..b37cc824f --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataResolver.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * Gets OpenIdMetadata. + */ +public interface OpenIdMetadataResolver { + + /** + * Gets OpenIdMetadata for the specified key. + * @param key The key. + * @return An OpenIdMetadata object. + */ + OpenIdMetadata get(String key); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ResponseFuture.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ResponseFuture.java deleted file mode 100644 index 61baa17c8..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ResponseFuture.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.microsoft.bot.connector.authentication; - -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Response; - -public class ResponseFuture implements Callback { - public final CompletableFuture future = new CompletableFuture(); - public Call call; - - public ResponseFuture(Call call) { - this.call = call; - } - - @Override public void onFailure(Call call, IOException e) { - future.completeExceptionally(e); - } - - @Override public void onResponse(Call call, Response response) throws IOException { - future.complete(response); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/Retry.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/Retry.java new file mode 100644 index 000000000..064dec5c3 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/Retry.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +/** + * Will retry a call for a configurable number of times with backoff. + * + * @see RetryParams + */ +public final class Retry { + private Retry() { + + } + + /** + * Runs a task with retry. + * + * @param task The task to run. + * @param retryExceptionHandler Called when an exception happens. + * @param The type of the result. + * @return A CompletableFuture that is complete when 'task' returns + * successfully. + * @throws RetryException If the task doesn't complete successfully. + */ + public static CompletableFuture run( + Supplier> task, + BiFunction retryExceptionHandler + ) { + return runInternal(task, retryExceptionHandler, 1, new ArrayList<>()); + } + + private static CompletableFuture runInternal( + Supplier> task, + BiFunction retryExceptionHandler, + final Integer retryCount, + final List exceptions + ) { + AtomicReference retry = new AtomicReference<>(); + + return task.get() + .exceptionally((t) -> { + exceptions.add(t); + retry.set(retryExceptionHandler.apply(new RetryException(t), retryCount)); + return null; + }) + .thenCompose(taskResult -> { + CompletableFuture result = new CompletableFuture<>(); + + if (retry.get() == null) { + result.complete(taskResult); + return result; + } + + if (retry.get().getShouldRetry()) { + try { + Thread.sleep(withBackOff(retry.get().getRetryAfter(), retryCount)); + } catch (InterruptedException e) { + throw new RetryException(e); + } + + return runInternal(task, retryExceptionHandler, retryCount + 1, exceptions); + } + + result.completeExceptionally(new RetryException("Exceeded retry count", exceptions)); + + return result; + }); + } + + private static final double BACKOFF_MULTIPLIER = 1.1; + + private static long withBackOff(long delay, int retryCount) { + double result = delay * Math.pow(BACKOFF_MULTIPLIER, retryCount - 1); + return (long) Math.min(result, Long.MAX_VALUE); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryAfterHelper.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryAfterHelper.java new file mode 100644 index 000000000..5c7c8db9e --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryAfterHelper.java @@ -0,0 +1,68 @@ +package com.microsoft.bot.connector.authentication; + +import java.time.Duration; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +/** + * Class that contains a helper function to process HTTP 429 Retry-After headers + * for the CredentialsAuthenticator. The reason to extract this was + * CredentialsAuthenticator is an internal class that isn't exposed except + * through other Authentication classes and we wanted a way to test the + * processing of 429 headers without building complicated test harnesses. + */ +public final class RetryAfterHelper { + + private RetryAfterHelper() { + + } + + /** + * Process a RetryException and see if we should wait for a requested amount of + * time before retrying to call the authentication service again. + * + * @param header The header values to process. + * @param count The count of how many times we have retried. + * @return A RetryParams with instructions of when or how many more times to + * retry. + */ + public static RetryParams processRetry(List header, Integer count) { + if (header == null || header.size() == 0) { + return RetryParams.defaultBackOff(++count); + } else { + String headerString = header.get(0); + if (StringUtils.isNotBlank(headerString)) { + // see if it matches a numeric value + if (headerString.matches("^[0-9]+\\.?0*$")) { + headerString = headerString.replaceAll("\\.0*$", ""); + Duration delay = Duration.ofSeconds(Long.parseLong(headerString)); + return new RetryParams(delay.toMillis()); + } else { + // check to see if it's a RFC_1123 format Date/Time + DateTimeFormatter gmtFormat = DateTimeFormatter.RFC_1123_DATE_TIME; + try { + ZonedDateTime zoned = ZonedDateTime.parse(headerString, gmtFormat); + if (zoned != null) { + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + long waitMillis = zoned.toInstant().toEpochMilli() - now.toInstant().toEpochMilli(); + if (waitMillis > 0) { + return new RetryParams(waitMillis); + } else { + return RetryParams.defaultBackOff(++count); + } + } + } catch (DateTimeParseException ex) { + return RetryParams.defaultBackOff(++count); + } + } + } + } + return RetryParams.defaultBackOff(++count); + } + +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryException.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryException.java new file mode 100644 index 000000000..5c3124e30 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryException.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import java.util.ArrayList; +import java.util.List; + +/** + * Retry exception when the Retry task fails to execute successfully. + */ +public class RetryException extends RuntimeException { + private List exceptions = new ArrayList<>(); + + /** + * A RetryException with description and list of exceptions. + * + * @param message The message. + * @param withExceptions The list of exceptions collected by {@link Retry}. + */ + public RetryException(String message, List withExceptions) { + super(message); + exceptions = withExceptions; + } + + /** + * A Retry failure caused by an unexpected failure. + * + * @param cause The caught exception. + */ + public RetryException(Throwable cause) { + super(cause); + } + + /** + * A List of exceptions encountered when executing the Retry task. + * + * @return The List of exceptions. + */ + public List getExceptions() { + return exceptions; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryParams.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryParams.java new file mode 100644 index 000000000..d98921bff --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/RetryParams.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +package com.microsoft.bot.connector.authentication; + +import java.time.Duration; + +/** + * State for Retry. + */ +public class RetryParams { + public static final int MAX_RETRIES = 10; + private static final Duration MAX_DELAY = Duration.ofSeconds(10); + private static final Duration DEFAULT_BACKOFF_TIME = Duration.ofMillis(50); + + private boolean shouldRetry = true; + private long retryAfter; + + /** + * Helper to create a RetryParams with a shouldRetry of false. + * + * @return A RetryParams that returns false for {@link #getShouldRetry()}. + */ + public static RetryParams stopRetrying() { + RetryParams retryParams = new RetryParams(); + retryParams.setShouldRetry(false); + return retryParams; + } + + /** + * Helper to create a RetryParams with the default backoff time. + * + * @param retryCount The number of times retry has happened. + * @return A RetryParams object with the proper backoff time. + */ + public static RetryParams defaultBackOff(int retryCount) { + return retryCount < MAX_RETRIES + ? new RetryParams(DEFAULT_BACKOFF_TIME.toMillis()) + : stopRetrying(); + } + + /** + * Default Retry options. + */ + public RetryParams() { + + } + + /** + * RetryParams with the specified delay. + * + * @param withRetryAfter Delay in milliseconds. + */ + public RetryParams(long withRetryAfter) { + setRetryAfter(Math.min(withRetryAfter, MAX_DELAY.toMillis())); + } + + /** + * Indicates whether a retry should happen. + * + * @return True if a retry should occur. + */ + public boolean getShouldRetry() { + return shouldRetry; + } + + /** + * Sets whether a retry should happen. + * + * @param withShouldRetry True for a retry. + */ + public void setShouldRetry(boolean withShouldRetry) { + this.shouldRetry = withShouldRetry; + } + + /** + * Retry delay. + * + * @return Delay in milliseconds. + */ + public long getRetryAfter() { + return retryAfter; + } + + /** + * Sets the retry delay. + * + * @param withRetryAfter Delay in milliseconds. + */ + public void setRetryAfter(long withRetryAfter) { + this.retryAfter = withRetryAfter; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleChannelProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleChannelProvider.java new file mode 100644 index 000000000..fdadb8f4a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleChannelProvider.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.CompletableFuture; + +/** + * A ChannelProvider with in-memory values. + */ +public class SimpleChannelProvider implements ChannelProvider { + private String channelService; + + /** + * Creates a SimpleChannelProvider with no ChannelService which will use Public + * Azure. + */ + public SimpleChannelProvider() { + + } + + /** + * Creates a SimpleChannelProvider with the specified ChannelService. + * + * @param withChannelService The ChannelService to use. Null or empty strings + * represent Public Azure, the string + * 'https://botframework.us' represents US Government + * Azure, and other values are for private channels. + */ + public SimpleChannelProvider(String withChannelService) { + this.channelService = withChannelService; + } + + /** + * Returns the channel service value. + * + * @return The channel service. + */ + @Override + public CompletableFuture getChannelService() { + return CompletableFuture.completedFuture(channelService); + } + + /** + * Indicates whether this is a Gov channel provider. + * + * @return True if Gov. + */ + @Override + public boolean isGovernment() { + return GovernmentAuthenticationConstants.CHANNELSERVICE.equalsIgnoreCase(channelService); + } + + /** + * Indicates whether this is public Azure. + * + * @return True if pubic Azure. + */ + @Override + public boolean isPublicAzure() { + return StringUtils.isEmpty(channelService); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java index cc3e0a119..90ad488d4 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SimpleCredentialProvider.java @@ -1,48 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.authentication; import org.apache.commons.lang3.StringUtils; + import java.util.concurrent.CompletableFuture; +/** + * A simple implementation of the CredentialProvider interface. + */ public class SimpleCredentialProvider implements CredentialProvider { private String appId; private String password; - + /** + * Initializes a new instance with empty credentials. + */ public SimpleCredentialProvider() { } - public SimpleCredentialProvider(String appId, String password) { - this.appId = appId; - this.password = password; + + /** + * Initializes a new instance with the provided credentials. + * + * @param withAppId The app ID. + * @param withPassword The app password. + */ + public SimpleCredentialProvider(String withAppId, String withPassword) { + appId = withAppId; + password = withPassword; } + /** + * Gets the app ID for this credential. + * + * @return The app id. + */ public String getAppId() { return this.appId; } - public void setAppId(String appId) { - this.appId = appId; + + /** + * Sets the app ID for this credential. + * + * @param witAppId The app id. + */ + public void setAppId(String witAppId) { + appId = witAppId; } + /** + * Gets the app password for this credential. + * + * @return The password. + */ public String getPassword() { return password; } - public void setPassword(String password) { - this.password = password; + + /** + * Sets the app password for this credential. + * + * @param withPassword The password. + */ + public void setPassword(String withPassword) { + password = withPassword; } + /** + * Validates an app ID. + * + * @param validateAppId The app ID to validate. + * @return If the task is successful, the result is true if appId is valid for + * the controller; otherwise, false. + */ @Override - public CompletableFuture isValidAppIdAsync(String appId) { - return CompletableFuture.completedFuture(appId == this.appId); + public CompletableFuture isValidAppId(String validateAppId) { + return CompletableFuture.completedFuture(StringUtils.equals(validateAppId, appId)); } + /** + * Gets the app password for a given bot app ID. + * + * @param validateAppId The ID of the app to get the password for. + * @return If the task is successful and the app ID is valid, the result + * contains the password; otherwise, null. + */ @Override - public CompletableFuture getAppPasswordAsync(String appId) { - return CompletableFuture.completedFuture((appId == this.appId) ? this.password : null); + public CompletableFuture getAppPassword(String validateAppId) { + return CompletableFuture + .completedFuture(StringUtils.equals(validateAppId, appId) ? password : null); } + /** + * Checks whether bot authentication is disabled. + * + * @return A task that represents the work queued to execute If the task is + * successful and bot authentication is disabled, the result is true; + * otherwise, false. + */ @Override - public CompletableFuture isAuthenticationDisabledAsync() { + public CompletableFuture isAuthenticationDisabled() { return CompletableFuture.completedFuture(StringUtils.isEmpty(this.appId)); } - - } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java new file mode 100644 index 000000000..ededd5a65 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.auth0.jwt.JWT; +import com.microsoft.bot.connector.Async; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; + +/** + * Validates JWT tokens sent to and from a Skill. + */ +@SuppressWarnings("PMD") +public final class SkillValidation { + + private SkillValidation() { + + } + + ///

+ /// TO SKILL FROM BOT and TO BOT FROM SKILL: Token validation parameters when + /// connecting a bot to a skill. + /// + private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters(true, + Stream.of( + // Auth v3.1, 1.0 token + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + // Auth v3.1, 2.0 token + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + // Auth v3.2, 1.0 token + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + // Auth v3.2, 2.0 token + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + // Auth for US Gov, 1.0 token + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", + // Auth for US Gov, 2.0 token + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", + // Auth for US Gov, 1.0 token + "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + // Auth for US Gov, 2.0 token + "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0") + .collect(Collectors.toList()), + false, // Audience validation takes place manually in code. + true, Duration.ofMinutes(5), true); + + /** + * Determines if a given Auth header is from from a skill to bot or bot to skill + * request. + * + * @param authHeader Bearer Token, in the "Bearer [Long String]" Format. + * @return True, if the token was issued for a skill to bot communication. + * Otherwise, false. + */ + public static boolean isSkillToken(String authHeader) { + if (!JwtTokenValidation.isValidTokenFormat(authHeader)) { + return false; + } + + // We know is a valid token, split it and work with it: + // [0] = "Bearer" + // [1] = "[Big Long String]" + String bearerToken = authHeader.split(" ")[1]; + + // Parse token + ClaimsIdentity identity = new ClaimsIdentity(JWT.decode(bearerToken)); + + return isSkillClaim(identity.claims()); + } + + /** + * Checks if the given list of claims represents a skill. + * + * A skill claim should contain: An {@link AuthenticationConstants.VERSION_CLAIM} + * claim. An {@link AuthenticationConstants.AUTIENCE_CLAIM} claim. An + * {@link AuthenticationConstants.APPID_CLAIM} claim (v1) or an a + * {@link AuthenticationConstants.AUTHORIZED_PARTY} claim (v2). And the appId + * claim should be different than the audience claim. When a channel (webchat, + * teams, etc.) invokes a bot, the {@link AuthenticationConstants.AUTIENCE_CLAIM} + * is set to {@link AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER} but + * when a bot calls another bot, the audience claim is set to the appId of the + * bot being invoked. The protocol supports v1 and v2 tokens: For v1 tokens, the + * {@link AuthenticationConstants.APPID_CLAIM} is present and set to the app Id + * of the calling bot. For v2 tokens, the + * {@link AuthenticationConstants.AUTHORIZED_PARTY} is present and set to the app + * Id of the calling bot. + * + * @param claims A list of claims. + * + * @return True if the list of claims is a skill claim, false if is not. + */ + public static Boolean isSkillClaim(Map claims) { + + for (Map.Entry entry : claims.entrySet()) { + if (entry.getValue() != null && entry.getValue().equals(AuthenticationConstants.ANONYMOUS_SKILL_APPID) + && entry.getKey().equals(AuthenticationConstants.APPID_CLAIM)) { + return true; + } + } + + Optional> version = claims.entrySet().stream() + .filter((x) -> x.getKey().equals(AuthenticationConstants.VERSION_CLAIM)).findFirst(); + if (!version.isPresent()) { + // Must have a version claim. + return false; + } + + Optional> audience = claims.entrySet().stream() + .filter((x) -> x.getKey().equals(AuthenticationConstants.AUDIENCE_CLAIM)).findFirst(); + + if (!audience.isPresent() + || AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER.equals(audience.get().getValue())) { + // The audience is https://api.botframework.com and not an appId. + return false; + } + + String appId = JwtTokenValidation.getAppIdFromClaims(claims); + if (StringUtils.isBlank(appId)) { + return false; + } + + // Skill claims must contain and app ID and the AppID must be different than the + // audience. + return !StringUtils.equals(appId, audience.get().getValue()); + } + + /** + * Validates that the incoming Auth Header is a token sent from a bot to a skill + * or from a skill to a bot. + * + * @param authHeader The raw HTTP header in the format: "Bearer + * [longString]". + * @param credentials The user defined set of valid credentials, such as the + * AppId. + * @param channelProvider The channelService value that distinguishes public + * Azure from US Government Azure. + * @param channelId The ID of the channel to validate. + * @param authConfig The authentication configuration. + * + * @return A {@link ClaimsIdentity} instance if the validation is successful. + */ + public static CompletableFuture authenticateChannelToken(String authHeader, + CredentialProvider credentials, ChannelProvider channelProvider, String channelId, + AuthenticationConfiguration authConfig) { + if (authConfig == null) { + return Async.completeExceptionally(new IllegalArgumentException("authConfig cannot be null.")); + } + + String openIdMetadataUrl = channelProvider != null && channelProvider.isGovernment() + ? GovernmentAuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL + : AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL; + + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor(TOKENVALIDATIONPARAMETERS, openIdMetadataUrl, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS); + + return tokenExtractor.getIdentity(authHeader, channelId, authConfig.requiredEndorsements()) + .thenCompose(identity -> { + return validateIdentity(identity, credentials).thenCompose(result -> { + return CompletableFuture.completedFuture(identity); + }); + }); + } + + /** + * Helper to validate a skills ClaimsIdentity. + * + * @param identity The ClaimsIdentity to validate. + * @param credentials The CredentialProvider. + * @return Nothing if success, otherwise a CompletionException + */ + public static CompletableFuture validateIdentity(ClaimsIdentity identity, CredentialProvider credentials) { + if (identity == null) { + // No valid identity. Not Authorized. + return Async.completeExceptionally(new AuthenticationException("Invalid Identity")); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + return Async.completeExceptionally(new AuthenticationException("Token Not Authenticated")); + } + + Optional> versionClaim = identity.claims().entrySet().stream() + .filter(item -> StringUtils.equals(AuthenticationConstants.VERSION_CLAIM, item.getKey())).findFirst(); + if (!versionClaim.isPresent()) { + // No version claim + return Async.completeExceptionally(new AuthenticationException( + AuthenticationConstants.VERSION_CLAIM + " claim is required on skill Tokens.")); + } + + // Look for the "aud" claim, but only if issued from the Bot Framework + Optional> audienceClaim = identity.claims().entrySet().stream() + .filter(item -> StringUtils.equals(AuthenticationConstants.AUDIENCE_CLAIM, item.getKey())).findFirst(); + if (!audienceClaim.isPresent() || StringUtils.isEmpty(audienceClaim.get().getValue())) { + // Claim is not present or doesn't have a value. Not Authorized. + return Async.completeExceptionally(new AuthenticationException( + AuthenticationConstants.AUDIENCE_CLAIM + " claim is required on skill Tokens.")); + } + + String appId = JwtTokenValidation.getAppIdFromClaims(identity.claims()); + if (StringUtils.isEmpty(appId)) { + return Async.completeExceptionally(new AuthenticationException("Invalid appId.")); + } + + return credentials.isValidAppId(audienceClaim.get().getValue()).thenApply(isValid -> { + if (!isValid) { + throw new AuthenticationException("Invalid audience."); + } + return null; + }); + } + + /** + * Creates a ClaimsIdentity for an anonymous (unauthenticated) skill. + * + * @return A ClaimsIdentity instance with authentication type set to + * AuthenticationConstants.AnonymousAuthType and a reserved + * AuthenticationConstants.AnonymousSkillAppId claim. + */ + public static ClaimsIdentity createAnonymousSkillClaim() { + Map claims = new HashMap<>(); + claims.put(AuthenticationConstants.APPID_CLAIM, AuthenticationConstants.ANONYMOUS_SKILL_APPID); + return new ClaimsIdentity(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, + AuthenticationConstants.ANONYMOUS_AUTH_TYPE, claims); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java index b27706033..c1658ee72 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java @@ -4,27 +4,101 @@ package com.microsoft.bot.connector.authentication; import java.time.Duration; -import java.util.ArrayList; import java.util.List; -import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToBotFromChannelTokenIssuer; - +/** + * Contains a set of parameters that are used when validating a token. + */ +@SuppressWarnings("checkstyle:VisibilityModifier") public class TokenValidationParameters { + /** + * Control if the issuer will be validated during token validation. + */ public boolean validateIssuer; + + /** + * Contains valid issuers that will be used to check against the token's issuer. + */ public List validIssuers; + + /** + * Control if the audience will be validated during token validation. + */ public boolean validateAudience; + + /** + * Control if the lifetime will be validated during token validation. + */ public boolean validateLifetime; + + /** + * Clock skew to apply when validating a time. + */ public Duration clockSkew; + + /** + * Value indicating whether a token can be considered valid if not signed. + */ public boolean requireSignedTokens; + /** + * Optional (and not recommended) Function to return OpenIdMetaData resolver + * for a given url. + */ + public OpenIdMetadataResolver issuerSigningKeyResolver; + + /** + * True to validate the signing cert. + */ + public boolean validateIssuerSigningKey = true; + + /** + * Default parameters. + */ public TokenValidationParameters() { } + /** + * Copy constructor. + * + * @param other The TokenValidationParameters to copy. + */ public TokenValidationParameters(TokenValidationParameters other) { - this(other.validateIssuer, other.validIssuers, other.validateAudience, other.validateLifetime, other.clockSkew, other.requireSignedTokens); + this( + other.validateIssuer, + other.validIssuers, + other.validateAudience, + other.validateLifetime, + other.clockSkew, + other.requireSignedTokens + ); + this.issuerSigningKeyResolver = other.issuerSigningKeyResolver; + this.validateIssuerSigningKey = other.validateIssuerSigningKey; } - public TokenValidationParameters(boolean validateIssuer, List validIssuers, boolean validateAudience, boolean validateLifetime, Duration clockSkew, boolean requireSignedTokens) { + /** + * + * @param validateIssuer Control if the issuer will be validated during + * token validation. + * @param validIssuers Contains valid issuers that will be used to check + * against the token's issuer. + * @param validateAudience Control if the audience will be validated during + * token validation. + * @param validateLifetime Control if the lifetime will be validated during + * token validation. + * @param clockSkew Clock skew to apply when validating a time. + * @param requireSignedTokens Value indicating whether a token can be considered + * valid if not signed. + */ + @SuppressWarnings("checkstyle:HiddenField") + public TokenValidationParameters( + boolean validateIssuer, + List validIssuers, + boolean validateAudience, + boolean validateLifetime, + Duration clockSkew, + boolean requireSignedTokens + ) { this.validateIssuer = validateIssuer; this.validIssuers = validIssuers; this.validateAudience = validateAudience; @@ -32,33 +106,4 @@ public TokenValidationParameters(boolean validateIssuer, List validIssue this.clockSkew = clockSkew; this.requireSignedTokens = requireSignedTokens; } - - static TokenValidationParameters toBotFromEmulatorTokenValidationParameters() { - return new TokenValidationParameters() {{ - this.validateIssuer = true; - this.validIssuers = new ArrayList() {{ - add("https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/"); - add("https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0"); - add("https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/"); - add("https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0"); - }}; - this.validateAudience = false; - this.validateLifetime = true; - this.clockSkew = Duration.ofMinutes(5); - this.requireSignedTokens = true; - }}; - } - - static TokenValidationParameters toBotFromChannelTokenValidationParameters() { - return new TokenValidationParameters() {{ - this.validateIssuer = true; - this.validIssuers = new ArrayList() {{ - add(ToBotFromChannelTokenIssuer); - }}; - this.validateAudience = false; - this.validateLifetime = true; - this.clockSkew = Duration.ofMinutes(5); - this.requireSignedTokens = true; - }}; - } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/package-info.java new file mode 100644 index 000000000..bade06394 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the implementation auth classes for com.microsoft.bot.connector.authentication. + */ +package com.microsoft.bot.connector.authentication; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/AttachmentsImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/AttachmentsImpl.java deleted file mode 100644 index ccef8de03..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/AttachmentsImpl.java +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for - * license information. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is - * regenerated. - */ - -package com.microsoft.bot.connector.implementation; - -import retrofit2.Retrofit; -import com.microsoft.bot.connector.Attachments; -import com.google.common.reflect.TypeToken; -import com.microsoft.bot.schema.models.AttachmentInfo; -import com.microsoft.bot.connector.models.ErrorResponseException; -import com.microsoft.rest.ServiceCallback; -import com.microsoft.rest.ServiceFuture; -import com.microsoft.rest.ServiceResponse; -import java.io.InputStream; -import java.io.IOException; -import okhttp3.ResponseBody; -import retrofit2.http.GET; -import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.Path; -import retrofit2.http.Streaming; -import retrofit2.Response; -import rx.functions.Func1; -import rx.Observable; - -/** - * An instance of this class provides access to all the operations defined - * in Attachments. - */ -public class AttachmentsImpl implements Attachments { - /** The Retrofit service to perform REST calls. */ - private AttachmentsService service; - /** The service client containing this operation class. */ - private ConnectorClientImpl client; - - /** - * Initializes an instance of AttachmentsImpl. - * - * @param retrofit the Retrofit instance built from a Retrofit Builder. - * @param client the instance of the service client containing this operation class. - */ - public AttachmentsImpl(Retrofit retrofit, ConnectorClientImpl client) { - this.service = retrofit.create(AttachmentsService.class); - this.client = client; - } - - /** - * The interface defining all the services for Attachments to be - * used by Retrofit to perform actually REST calls. - */ - interface AttachmentsService { - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Attachments getAttachmentInfo" }) - @GET("v3/attachments/{attachmentId}") - Observable> getAttachmentInfo(@Path("attachmentId") String attachmentId, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Attachments getAttachment" }) - @GET("v3/attachments/{attachmentId}/views/{viewId}") - @Streaming - Observable> getAttachment(@Path("attachmentId") String attachmentId, @Path("viewId") String viewId, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - } - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the AttachmentInfo object if successful. - */ - public AttachmentInfo getAttachmentInfo(String attachmentId) { - return getAttachmentInfoWithServiceResponseAsync(attachmentId).toBlocking().single().body(); - } - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture getAttachmentInfoAsync(String attachmentId, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(getAttachmentInfoWithServiceResponseAsync(attachmentId), serviceCallback); - } - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the AttachmentInfo object - */ - public Observable getAttachmentInfoAsync(String attachmentId) { - return getAttachmentInfoWithServiceResponseAsync(attachmentId).map(new Func1, AttachmentInfo>() { - @Override - public AttachmentInfo call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * GetAttachmentInfo. - * Get AttachmentInfo structure describing the attachment views. - * - * @param attachmentId attachment id - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the AttachmentInfo object - */ - public Observable> getAttachmentInfoWithServiceResponseAsync(String attachmentId) { - if (attachmentId == null) { - throw new IllegalArgumentException("Parameter attachmentId is required and cannot be null."); - } - return service.getAttachmentInfo(attachmentId, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = getAttachmentInfoDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse getAttachmentInfoDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the InputStream object if successful. - */ - public InputStream getAttachment(String attachmentId, String viewId) { - return getAttachmentWithServiceResponseAsync(attachmentId, viewId).toBlocking().single().body(); - } - - /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture getAttachmentAsync(String attachmentId, String viewId, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(getAttachmentWithServiceResponseAsync(attachmentId, viewId), serviceCallback); - } - - /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the InputStream object - */ - public Observable getAttachmentAsync(String attachmentId, String viewId) { - return getAttachmentWithServiceResponseAsync(attachmentId, viewId).map(new Func1, InputStream>() { - @Override - public InputStream call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * GetAttachment. - * Get the named view as binary content. - * - * @param attachmentId attachment id - * @param viewId View id from attachmentInfo - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the InputStream object - */ - public Observable> getAttachmentWithServiceResponseAsync(String attachmentId, String viewId) { - if (attachmentId == null) { - throw new IllegalArgumentException("Parameter attachmentId is required and cannot be null."); - } - if (viewId == null) { - throw new IllegalArgumentException("Parameter viewId is required and cannot be null."); - } - return service.getAttachment(attachmentId, viewId, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = getAttachmentDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse getAttachmentDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(301, new TypeToken() { }.getType()) - .register(302, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConnectorClientImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConnectorClientImpl.java deleted file mode 100644 index 042ac1d86..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConnectorClientImpl.java +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for - * license information. - * - */ - -package com.microsoft.bot.connector.implementation; - -import com.microsoft.azure.AzureClient; -import com.microsoft.azure.AzureServiceClient; -import com.microsoft.bot.connector.Attachments; -import com.microsoft.bot.connector.ConnectorClient; -import com.microsoft.bot.connector.Conversations; -import com.microsoft.rest.credentials.ServiceClientCredentials; -import com.microsoft.rest.RestClient; -import com.microsoft.rest.retry.RetryStrategy; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; -import java.util.stream.Stream; - -/** - * Initializes a new instance of the ConnectorClientImpl class. - */ -public class ConnectorClientImpl extends AzureServiceClient implements ConnectorClient { - /** the {@link AzureClient} used for long running operations. */ - private AzureClient azureClient; - - - /** - * Gets the {@link AzureClient} used for long running operations. - * @return the azure client; - */ - public AzureClient getAzureClient() { - return this.azureClient; - } - - /** Gets or sets the preferred language for the response. */ - private String acceptLanguage; - private String user_agent_string; - - /** - * Gets Gets or sets the preferred language for the response. - * - * @return the acceptLanguage value. - */ - public String acceptLanguage() { - return this.acceptLanguage; - } - - /** - * Sets Gets or sets the preferred language for the response. - * - * @param acceptLanguage the acceptLanguage value. - * @return the service client itself - */ - public ConnectorClientImpl withAcceptLanguage(String acceptLanguage) { - this.acceptLanguage = acceptLanguage; - return this; - } - - /** - * RetryStrategy as defined in Microsoft Rest Retry - * TODO: Use this. - */ - private RetryStrategy retryStrategy = null; - public ConnectorClientImpl withRestRetryStrategy(RetryStrategy retryStrategy) { - this.retryStrategy = retryStrategy; - return this; - } - public RetryStrategy restRetryStrategy() { - return this.retryStrategy; - } - - /** Gets or sets the retry timeout in seconds for Long Running Operations. Default value is 30. */ - private int longRunningOperationRetryTimeout; - - /** - * Gets Gets or sets the retry timeout in seconds for Long Running Operations. Default value is 30. - * - * @return the longRunningOperationRetryTimeout value. - */ - public int longRunningOperationRetryTimeout() { - return this.longRunningOperationRetryTimeout; - } - - /** - * Sets Gets or sets the retry timeout in seconds for Long Running Operations. Default value is 30. - * - * @param longRunningOperationRetryTimeout the longRunningOperationRetryTimeout value. - * @return the service client itself - */ - public ConnectorClientImpl withLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout) { - this.longRunningOperationRetryTimeout = longRunningOperationRetryTimeout; - return this; - } - - /** When set to true a unique x-ms-client-request-id value is generated and included in each request. Default is true. */ - private boolean generateClientRequestId; - - /** - * Gets When set to true a unique x-ms-client-request-id value is generated and included in each request. Default is true. - * - * @return the generateClientRequestId value. - */ - public boolean generateClientRequestId() { - return this.generateClientRequestId; - } - - /** - * Sets When set to true a unique x-ms-client-request-id value is generated and included in each request. Default is true. - * - * @param generateClientRequestId the generateClientRequestId value. - * @return the service client itself - */ - public ConnectorClientImpl withGenerateClientRequestId(boolean generateClientRequestId) { - this.generateClientRequestId = generateClientRequestId; - return this; - } - - /** - * The Attachments object to access its operations. - */ - private Attachments attachments; - - /** - * Gets the Attachments object to access its operations. - * @return the Attachments object. - */ - public Attachments attachments() { - return this.attachments; - } - - /** - * The Conversations object to access its operations. - */ - private ConversationsImpl conversations; - - /** - * Gets the Conversations object to access its operations. - * @return the Conversations object. - */ - @Override - public ConversationsImpl conversations() { - return this.conversations; - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param credentials the management credentials for Azure - */ - public ConnectorClientImpl(ServiceClientCredentials credentials) { - this("https://api.botframework.com", credentials); - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param baseUrl the base URL of the host - * @param credentials the management credentials for Azure - */ - public ConnectorClientImpl(String baseUrl, ServiceClientCredentials credentials) { - super(baseUrl, credentials); - initialize(); - } - - /** - * Initializes an instance of ConnectorClient client. - * - * @param restClient the REST client to connect to Azure. - */ - public ConnectorClientImpl(RestClient restClient){ - super(restClient); - initialize(); - } - - protected void initialize() { - this.acceptLanguage = "en-US"; - this.longRunningOperationRetryTimeout = 30; - this.generateClientRequestId = true; - this.attachments = new AttachmentsImpl(restClient().retrofit(), this); - this.conversations = new ConversationsImpl(restClient().retrofit(), this); - this.azureClient = new AzureClient(this); - - - // Format according to https://github.com/Microsoft/botbuilder-dotnet/blob/d342cd66d159a023ac435aec0fdf791f93118f5f/doc/UserAgents.md - String build_version; - final Properties properties = new Properties(); - try { - InputStream propStream = ConnectorClientImpl.class.getClassLoader().getResourceAsStream("project.properties"); - properties.load(propStream); - build_version = properties.getProperty("version"); - } catch (IOException e) { - e.printStackTrace(); - build_version = "4.0.0"; - } - - String os_version = System.getProperty("os.name"); - String java_version = System.getProperty("java.version"); - this.user_agent_string = String.format("BotBuilder/%s (JVM %s; %s)", build_version, java_version, os_version); - } - - - /** - * Gets the User-Agent header for the client. - * - * @return the user agent string. - */ - - @Override - public String userAgent() { - return this.user_agent_string; - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConversationsImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConversationsImpl.java deleted file mode 100644 index b8889cec5..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/ConversationsImpl.java +++ /dev/null @@ -1,1205 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for - * license information. - * - * NOT GENERATED. - * This uses Java 8 CompletionStage for async processing instead of JavaRX/Guava - */ - -package com.microsoft.bot.connector.implementation; - -import retrofit2.Retrofit; -import com.microsoft.bot.connector.Conversations; -import com.google.common.reflect.TypeToken; -import com.microsoft.bot.schema.models.Activity; -import com.microsoft.bot.schema.models.AttachmentData; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.bot.schema.models.ConversationParameters; -import com.microsoft.bot.schema.models.ConversationResourceResponse; -import com.microsoft.bot.schema.models.ConversationsResult; -import com.microsoft.bot.connector.models.ErrorResponseException; -import com.microsoft.bot.schema.models.ResourceResponse; -import com.microsoft.rest.ServiceCallback; -import com.microsoft.rest.ServiceFuture; -import com.microsoft.rest.ServiceResponse; -import com.microsoft.rest.Validator; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import okhttp3.ResponseBody; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.HTTP; -import retrofit2.http.Path; -import retrofit2.http.POST; -import retrofit2.http.PUT; -import retrofit2.http.Query; -import retrofit2.Response; -import rx.functions.Func1; -import rx.Observable; - -/** - * An instance of this class provides access to all the operations defined - * in Conversations. - */ -public class ConversationsImpl implements Conversations { - /** The Retrofit service to perform REST calls. */ - private ConversationsService service; - /** The service client containing this operation class. */ - private ConnectorClientImpl client; - - /** - * Initializes an instance of ConversationsImpl. - * - * @param retrofit the Retrofit instance built from a Retrofit Builder. - * @param client the instance of the service client containing this operation class. - */ - public ConversationsImpl(Retrofit retrofit, ConnectorClientImpl client) { - this.service = retrofit.create(ConversationsService.class); - this.client = client; - } - - /** - * The interface defining all the services for Conversations to be - * used by Retrofit to perform actually REST calls. - */ - interface ConversationsService { - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversations" }) - @GET("v3/conversations") - Observable> getConversations(@Query("continuationToken") String continuationToken, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations createConversation" }) - @POST("v3/conversations") - Observable> createConversation(@Body ConversationParameters parameters, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations sendToConversation" }) - @POST("v3/conversations/{conversationId}/activities") - Observable> sendToConversation(@Path("conversationId") String conversationId, @Body Activity activity, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations updateActivity" }) - @PUT("v3/conversations/{conversationId}/activities/{activityId}") - Observable> updateActivity(@Path("conversationId") String conversationId, @Path("activityId") String activityId, @Body Activity activity, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations replyToActivity" }) - @POST("v3/conversations/{conversationId}/activities/{activityId}") - Observable> replyToActivity(@Path("conversationId") String conversationId, @Path("activityId") String activityId, @Body Activity activity, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations deleteActivity" }) - @HTTP(path = "v3/conversations/{conversationId}/activities/{activityId}", method = "DELETE", hasBody = true) - Observable> deleteActivity(@Path("conversationId") String conversationId, @Path("activityId") String activityId, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversationMembers" }) - @GET("v3/conversations/{conversationId}/members") - Observable> getConversationMembers(@Path("conversationId") String conversationId, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations deleteConversationMember" }) - @HTTP(path = "v3/conversations/{conversationId}/members/{memberId}", method = "DELETE", hasBody = true) - Observable> deleteConversationMember(@Path("conversationId") String conversationId, @Path("memberId") String memberId, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations getActivityMembers" }) - @GET("v3/conversations/{conversationId}/activities/{activityId}/members") - Observable> getActivityMembers(@Path("conversationId") String conversationId, @Path("activityId") String activityId, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - @Headers({ "Content-Type: application/json; charset=utf-8", "x-ms-logging-context: com.microsoft.bot.schema.Conversations uploadAttachment" }) - @POST("v3/conversations/{conversationId}/attachments") - Observable> uploadAttachment(@Path("conversationId") String conversationId, @Body AttachmentData attachmentUpload, @Header("accept-language") String acceptLanguage, @Header("User-Agent") String userAgent); - - } - - public static CompletableFuture> completableFutureFromObservable(Observable observable) { - final CompletableFuture> future = new CompletableFuture<>(); - observable - .doOnError(future::completeExceptionally) - .toList() - .forEach(future::complete); - return future; - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ConversationsResult object if successful. - */ - public ConversationsResult getConversations() { - return getConversationsWithServiceResponseAsync().toBlocking().single().body(); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture getConversationsAsync(final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(getConversationsWithServiceResponseAsync(), serviceCallback); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationsResult object - */ - public Observable getConversationsAsync() { - return getConversationsWithServiceResponseAsync().map(new Func1, ConversationsResult>() { - @Override - public ConversationsResult call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationsResult object - */ - public Observable> getConversationsWithServiceResponseAsync() { - final String continuationToken = null; - return service.getConversations(continuationToken, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = getConversationsDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param continuationToken skip or continuation token - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ConversationsResult object if successful. - */ - public ConversationsResult getConversations(String continuationToken) { - return getConversationsWithServiceResponseAsync(continuationToken).toBlocking().single().body(); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param continuationToken skip or continuation token - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture getConversationsAsync(String continuationToken, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(getConversationsWithServiceResponseAsync(continuationToken), serviceCallback); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param continuationToken skip or continuation token - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationsResult object - */ - public Observable getConversationsAsync(String continuationToken) { - return getConversationsWithServiceResponseAsync(continuationToken).map(new Func1, ConversationsResult>() { - @Override - public ConversationsResult call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * GetConversations. - * List the Conversations in which this bot has participated. - GET from this method with a skip token - The return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token. If the skip token is not empty, then - there are further values to be returned. Call this method again with the returned token to get more values. - Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation. - * - * @param continuationToken skip or continuation token - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationsResult object - */ - public Observable> getConversationsWithServiceResponseAsync(String continuationToken) { - return service.getConversations(continuationToken, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = getConversationsDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse getConversationsDelegate(Response response) throws ErrorResponseException, IOException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ConversationResourceResponse object if successful. - */ - public ConversationResourceResponse createConversation(ConversationParameters parameters) { - return createConversationWithServiceResponseAsync(parameters).toBlocking().single().body(); - } - - /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture createConversationAsync(ConversationParameters parameters, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(createConversationWithServiceResponseAsync(parameters), serviceCallback); - } - - /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationResourceResponse object - */ - public Observable createConversationAsync(ConversationParameters parameters) { - return createConversationWithServiceResponseAsync(parameters).map(new Func1, ConversationResourceResponse>() { - @Override - public ConversationResourceResponse call(ServiceResponse response) { - return response.body(); - } - }); - } - - public CompletableFuture> CreateConversationAsync(ConversationParameters parameters) { - CompletableFuture> future_result = completableFutureFromObservable(createConversationAsync(parameters)); - return future_result; - } - - - /** - * CreateConversation. - * Create a new Conversation. - POST to this method with a - * Bot being the bot creating the conversation - * IsGroup set to true if this is not a direct message (default is false) - * Members array contining the members you want to have be in the conversation. - The return value is a ResourceResponse which contains a conversation id which is suitable for use - in the message payload and REST API uris. - Most channels only support the semantics of bots initiating a direct message conversation. An example of how to do that would be: - ``` - var resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount("user1") } ); - await connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ; - ```. - * - * @param parameters Parameters to create the conversation from - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ConversationResourceResponse object - */ - public Observable> createConversationWithServiceResponseAsync(ConversationParameters parameters) { - if (parameters == null) { - throw new IllegalArgumentException("Parameter parameters is required and cannot be null."); - } - Validator.validate(parameters); - return service.createConversation(parameters, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = createConversationDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse createConversationDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(201, new TypeToken() { }.getType()) - .register(202, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - public ResourceResponse sendToConversation(String conversationId, Activity activity) { - return sendToConversationWithServiceResponseAsync(conversationId, activity).toBlocking().single().body(); - } - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activity Activity to send - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture sendToConversationAsync(String conversationId, Activity activity, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(sendToConversationWithServiceResponseAsync(conversationId, activity), serviceCallback); - } - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable sendToConversationAsync(String conversationId, Activity activity) { - return sendToConversationWithServiceResponseAsync(conversationId, activity).map(new Func1, ResourceResponse>() { - @Override - public ResourceResponse call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * SendToConversation. - * This method allows you to send an activity to the end of a conversation. - This is slightly different from ReplyToActivity(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable> sendToConversationWithServiceResponseAsync(String conversationId, Activity activity) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (activity == null) { - throw new IllegalArgumentException("Parameter activity is required and cannot be null."); - } - Validator.validate(activity); - return service.sendToConversation(conversationId, activity, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = sendToConversationDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse sendToConversationDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(201, new TypeToken() { }.getType()) - .register(202, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. - * - * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - public ResourceResponse updateActivity(String conversationId, String activityId, Activity activity) { - return updateActivityWithServiceResponseAsync(conversationId, activityId, activity).toBlocking().single().body(); - } - - /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. - * - * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture updateActivityAsync(String conversationId, String activityId, Activity activity, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(updateActivityWithServiceResponseAsync(conversationId, activityId, activity), serviceCallback); - } - - /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. - * - * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable updateActivityAsync(String conversationId, String activityId, Activity activity) { - return updateActivityWithServiceResponseAsync(conversationId, activityId, activity).map(new Func1, ResourceResponse>() { - @Override - public ResourceResponse call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * UpdateActivity. - * Edit an existing activity. - Some channels allow you to edit an existing activity to reflect the new state of a bot conversation. - For example, you can remove buttons after someone has clicked "Approve" button. - * - * @param conversationId Conversation ID - * @param activityId activityId to update - * @param activity replacement Activity - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable> updateActivityWithServiceResponseAsync(String conversationId, String activityId, Activity activity) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (activityId == null) { - throw new IllegalArgumentException("Parameter activityId is required and cannot be null."); - } - if (activity == null) { - throw new IllegalArgumentException("Parameter activity is required and cannot be null."); - } - Validator.validate(activity); - return service.updateActivity(conversationId, activityId, activity, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = updateActivityDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse updateActivityDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(201, new TypeToken() { }.getType()) - .register(202, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - public ResourceResponse replyToActivity(String conversationId, String activityId, Activity activity) { - return replyToActivityWithServiceResponseAsync(conversationId, activityId, activity).toBlocking().single().body(); - } - - /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture replyToActivityAsync(String conversationId, String activityId, Activity activity, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(replyToActivityWithServiceResponseAsync(conversationId, activityId, activity), serviceCallback); - } - - /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable replyToActivityAsync(String conversationId, String activityId, Activity activity) { - return replyToActivityWithServiceResponseAsync(conversationId, activityId, activity).map(new Func1, ResourceResponse>() { - @Override - public ResourceResponse call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * ReplyToActivity. - * This method allows you to reply to an activity. - This is slightly different from SendToConversation(). - * SendToConverstion(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel. - * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation. - Use ReplyToActivity when replying to a specific activity in the conversation. - Use SendToConversation in all other cases. - * - * @param conversationId Conversation ID - * @param activityId activityId the reply is to (OPTIONAL) - * @param activity Activity to send - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable> replyToActivityWithServiceResponseAsync(String conversationId, String activityId, Activity activity) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (activityId == null) { - throw new IllegalArgumentException("Parameter activityId is required and cannot be null."); - } - if (activity == null) { - throw new IllegalArgumentException("Parameter activity is required and cannot be null."); - } - Validator.validate(activity); - return service.replyToActivity(conversationId, activityId, activity, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = replyToActivityDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse replyToActivityDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(201, new TypeToken() { }.getType()) - .register(202, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. - * - * @param conversationId Conversation ID - * @param activityId activityId to delete - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - */ - public void deleteActivity(String conversationId, String activityId) { - deleteActivityWithServiceResponseAsync(conversationId, activityId).toBlocking().single().body(); - } - - /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. - * - * @param conversationId Conversation ID - * @param activityId activityId to delete - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture deleteActivityAsync(String conversationId, String activityId, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(deleteActivityWithServiceResponseAsync(conversationId, activityId), serviceCallback); - } - - /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. - * - * @param conversationId Conversation ID - * @param activityId activityId to delete - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - public Observable deleteActivityAsync(String conversationId, String activityId) { - return deleteActivityWithServiceResponseAsync(conversationId, activityId).map(new Func1, Void>() { - @Override - public Void call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * DeleteActivity. - * Delete an existing activity. - Some channels allow you to delete an existing activity, and if successful this method will remove the specified activity. - * - * @param conversationId Conversation ID - * @param activityId activityId to delete - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - public Observable> deleteActivityWithServiceResponseAsync(String conversationId, String activityId) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (activityId == null) { - throw new IllegalArgumentException("Parameter activityId is required and cannot be null."); - } - return service.deleteActivity(conversationId, activityId, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = deleteActivityDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse deleteActivityDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(202, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. - * - * @param conversationId Conversation ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the List<ChannelAccount> object if successful. - */ - public List getConversationMembers(String conversationId) { - return getConversationMembersWithServiceResponseAsync(conversationId).toBlocking().single().body(); - } - - /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. - * - * @param conversationId Conversation ID - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture> getConversationMembersAsync(String conversationId, final ServiceCallback> serviceCallback) { - return ServiceFuture.fromResponse(getConversationMembersWithServiceResponseAsync(conversationId), serviceCallback); - } - - /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. - * - * @param conversationId Conversation ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object - */ - public Observable> getConversationMembersAsync(String conversationId) { - return getConversationMembersWithServiceResponseAsync(conversationId).map(new Func1>, List>() { - @Override - public List call(ServiceResponse> response) { - return response.body(); - } - }); - } - - /** - * GetConversationMembers. - * Enumerate the members of a converstion. - This REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation. - * - * @param conversationId Conversation ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object - */ - public Observable>> getConversationMembersWithServiceResponseAsync(String conversationId) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - return service.getConversationMembers(conversationId, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>>() { - @Override - public Observable>> call(Response response) { - try { - ServiceResponse> clientResponse = getConversationMembersDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse> getConversationMembersDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory()., ErrorResponseException>newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken>() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. - * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - */ - public void deleteConversationMember(String conversationId, String memberId) { - deleteConversationMemberWithServiceResponseAsync(conversationId, memberId).toBlocking().single().body(); - } - - /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. - * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture deleteConversationMemberAsync(String conversationId, String memberId, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(deleteConversationMemberWithServiceResponseAsync(conversationId, memberId), serviceCallback); - } - - /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. - * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - public Observable deleteConversationMemberAsync(String conversationId, String memberId) { - return deleteConversationMemberWithServiceResponseAsync(conversationId, memberId).map(new Func1, Void>() { - @Override - public Void call(ServiceResponse response) { - return response.body(); - } - }); - } - /** - * DeleteConversationMemberFuture - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. - * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return CompletableFuture of List < Void > - */ - public CompletableFuture> deleteConversationMemberFuture(String conversationId, String memberId) throws ExecutionException, InterruptedException { - CompletableFuture> future_result = completableFutureFromObservable(deleteConversationMemberAsync(conversationId, memberId)); - return future_result; - } - /** - * DeleteConversationMember. - * Deletes a member from a converstion. - This REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member - of the conversation, the conversation will also be deleted. - * - * @param conversationId Conversation ID - * @param memberId ID of the member to delete from this conversation - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceResponse} object if successful. - */ - public Observable> deleteConversationMemberWithServiceResponseAsync(String conversationId, String memberId) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (memberId == null) { - throw new IllegalArgumentException("Parameter memberId is required and cannot be null."); - } - return service.deleteConversationMember(conversationId, memberId, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = deleteConversationMemberDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - - private ServiceResponse deleteConversationMemberDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(204, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. - * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the List<ChannelAccount> object if successful. - */ - public List getActivityMembers(String conversationId, String activityId) { - return getActivityMembersWithServiceResponseAsync(conversationId, activityId).toBlocking().single().body(); - } - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. - * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture> getActivityMembersAsync(String conversationId, String activityId, final ServiceCallback> serviceCallback) { - return ServiceFuture.fromResponse(getActivityMembersWithServiceResponseAsync(conversationId, activityId), serviceCallback); - } - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. - * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object - */ - public Observable> getActivityMembersAsync(String conversationId, String activityId) { - return getActivityMembersWithServiceResponseAsync(conversationId, activityId).map(new Func1>, List>() { - @Override - public List call(ServiceResponse> response) { - return response.body(); - } - }); - } - - /** - * GetActivityMembers. - * Enumerate the members of an activity. - This REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation. - * - * @param conversationId Conversation ID - * @param activityId Activity ID - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the List<ChannelAccount> object - */ - public Observable>> getActivityMembersWithServiceResponseAsync(String conversationId, String activityId) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (activityId == null) { - throw new IllegalArgumentException("Parameter activityId is required and cannot be null."); - } - return service.getActivityMembers(conversationId, activityId, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>>() { - @Override - public Observable>> call(Response response) { - try { - ServiceResponse> clientResponse = getActivityMembersDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse> getActivityMembersDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory()., ErrorResponseException>newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken>() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. - * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @throws IllegalArgumentException thrown if parameters fail the validation - * @throws ErrorResponseException thrown if the request is rejected by server - * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent - * @return the ResourceResponse object if successful. - */ - public ResourceResponse uploadAttachment(String conversationId, AttachmentData attachmentUpload) { - return uploadAttachmentWithServiceResponseAsync(conversationId, attachmentUpload).toBlocking().single().body(); - } - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. - * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @param serviceCallback the async ServiceCallback to handle successful and failed responses. - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the {@link ServiceFuture} object - */ - public ServiceFuture uploadAttachmentAsync(String conversationId, AttachmentData attachmentUpload, final ServiceCallback serviceCallback) { - return ServiceFuture.fromResponse(uploadAttachmentWithServiceResponseAsync(conversationId, attachmentUpload), serviceCallback); - } - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. - * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable uploadAttachmentAsync(String conversationId, AttachmentData attachmentUpload) { - return uploadAttachmentWithServiceResponseAsync(conversationId, attachmentUpload).map(new Func1, ResourceResponse>() { - @Override - public ResourceResponse call(ServiceResponse response) { - return response.body(); - } - }); - } - - /** - * UploadAttachment. - * Upload an attachment directly into a channel's blob storage. - This is useful because it allows you to store data in a compliant store when dealing with enterprises. - The response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API. - * - * @param conversationId Conversation ID - * @param attachmentUpload Attachment data - * @throws IllegalArgumentException thrown if parameters fail the validation - * @return the observable to the ResourceResponse object - */ - public Observable> uploadAttachmentWithServiceResponseAsync(String conversationId, AttachmentData attachmentUpload) { - if (conversationId == null) { - throw new IllegalArgumentException("Parameter conversationId is required and cannot be null."); - } - if (attachmentUpload == null) { - throw new IllegalArgumentException("Parameter attachmentUpload is required and cannot be null."); - } - Validator.validate(attachmentUpload); - return service.uploadAttachment(conversationId, attachmentUpload, this.client.acceptLanguage(), this.client.userAgent()) - .flatMap(new Func1, Observable>>() { - @Override - public Observable> call(Response response) { - try { - ServiceResponse clientResponse = uploadAttachmentDelegate(response); - return Observable.just(clientResponse); - } catch (Throwable t) { - return Observable.error(t); - } - } - }); - } - - private ServiceResponse uploadAttachmentDelegate(Response response) throws ErrorResponseException, IOException, IllegalArgumentException { - return this.client.restClient().responseBuilderFactory().newInstance(this.client.serializerAdapter()) - .register(200, new TypeToken() { }.getType()) - .register(201, new TypeToken() { }.getType()) - .register(202, new TypeToken() { }.getType()) - .registerError(ErrorResponseException.class) - .build(response); - } - -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/futureFromObservable.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/futureFromObservable.java deleted file mode 100644 index 4b326a809..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/futureFromObservable.java +++ /dev/null @@ -1,2 +0,0 @@ -package com.microsoft.bot.connector.implementation; - diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/package-info.java deleted file mode 100644 index e57157f5f..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/implementation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -// -// Code generated by Microsoft (R) AutoRest Code Generator. -// Changes may cause incorrect behavior and will be lost if the code is -// regenerated. - -/** - * This package contains the implementation classes for ConnectorClient. - * The Bot Connector REST API allows your bot to send and receive messages to channels configured in the - [Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses industry-standard REST - and JSON over HTTPS. - Client libraries for this REST API are available. See below for a list. - Many bots will use both the Bot Connector REST API and the associated [Bot State REST API](/en-us/restapi/state). The - Bot State REST API allows a bot to store and retrieve state associated with users and conversations. - Authentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is - described in detail in the [Connector Authentication](/en-us/restapi/authentication) document. - # Client Libraries for the Bot Connector REST API - * [Bot Builder for C#](/en-us/csharp/builder/sdkreference/) - * [Bot Builder for Node.js](/en-us/node/builder/overview/) - * Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json) - © 2016 Microsoft. - */ -package com.microsoft.bot.connector.implementation; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/models/ErrorResponseException.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/models/ErrorResponseException.java deleted file mode 100644 index 7e4a0f19f..000000000 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/models/ErrorResponseException.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for - * license information. - * - * Code generated by Microsoft (R) AutoRest Code Generator. - * Changes may cause incorrect behavior and will be lost if the code is - * regenerated. - */ - -package com.microsoft.bot.connector.models; - -import com.microsoft.rest.RestException;import com.microsoft.bot.schema.models.ErrorResponse; -import okhttp3.ResponseBody; -import retrofit2.Response; - -/** - * Exception thrown for an invalid response with ErrorResponse information. - */ -public class ErrorResponseException extends RestException { - /** - * Initializes a new instance of the ErrorResponseException class. - * - * @param message the exception message or the response content if a message is not available - * @param response the HTTP response - */ - public ErrorResponseException(final String message, final Response response) { - super(message, response); - } - - /** - * Initializes a new instance of the ErrorResponseException class. - * - * @param message the exception message or the response content if a message is not available - * @param response the HTTP response - * @param body the deserialized response body - */ - public ErrorResponseException(final String message, final Response response, final ErrorResponse body) { - super(message, response, body); - } - - @Override - public ErrorResponse body() { - return (ErrorResponse) super.body(); - } -} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java index 7517691b8..6b974b45b 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/package-info.java @@ -1,25 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for // license information. -// -// Code generated by Microsoft (R) AutoRest Code Generator. -// Changes may cause incorrect behavior and will be lost if the code is -// regenerated. /** - * This package contains the classes for ConnectorClient. - * The Bot Connector REST API allows your bot to send and receive messages to channels configured in the - [Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses industry-standard REST - and JSON over HTTPS. - Client libraries for this REST API are available. See below for a list. - Many bots will use both the Bot Connector REST API and the associated [Bot State REST API](/en-us/restapi/state). The - Bot State REST API allows a bot to store and retrieve state associated with users and conversations. - Authentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is - described in detail in the [Connector Authentication](/en-us/restapi/authentication) document. - # Client Libraries for the Bot Connector REST API - * [Bot Builder for C#](/en-us/csharp/builder/sdkreference/) - * [Bot Builder for Node.js](/en-us/node/builder/overview/) - * Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json) - © 2016 Microsoft. + * This package contains the classes for com.microsoft.bot.connector. */ +@Deprecated package com.microsoft.bot.connector; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/ErrorResponseException.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/ErrorResponseException.java new file mode 100644 index 000000000..ce40b6f09 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/ErrorResponseException.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.restclient.RestException; +import com.microsoft.bot.schema.ErrorResponse; +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * Exception thrown for an invalid response with ErrorResponse information. + */ +public class ErrorResponseException extends RestException { + /** + * Initializes a new instance of the ErrorResponseException class. + * + * @param message the exception message or the response content if a message is + * not available + * @param response the HTTP response + */ + public ErrorResponseException(final String message, final Response response) { + super(message, response); + } + + /** + * Initializes a new instance of the ErrorResponseException class. + * + * @param message the exception message or the response content if a message is + * not available + * @param response the HTTP response + * @param body the deserialized response body + */ + public ErrorResponseException( + final String message, + final Response response, + final ErrorResponse body + ) { + super(message, response, body); + } + + /** + * The HTTP response body. + * + * @return the HTTP response body + */ + @Override + public ErrorResponse body() { + return (ErrorResponse) super.body(); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestAttachments.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestAttachments.java new file mode 100644 index 000000000..7fbc2d006 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestAttachments.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.connector.Async; +import java.net.URLEncoder; +import org.apache.commons.lang3.StringUtils; +import retrofit2.Retrofit; +import com.microsoft.bot.connector.Attachments; +import com.google.common.reflect.TypeToken; +import com.microsoft.bot.schema.AttachmentInfo; +import com.microsoft.bot.restclient.ServiceResponse; +import java.io.InputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.concurrent.CompletableFuture; + +import okhttp3.ResponseBody; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.Path; +import retrofit2.http.Streaming; +import retrofit2.Response; + +/** + * An instance of this class provides access to all the operations defined in + * Attachments. + */ +public class RestAttachments implements Attachments { + /** The Retrofit service to perform REST calls. */ + private AttachmentsService service; + /** The service client containing this operation class. */ + private RestConnectorClient client; + + /** + * Initializes an instance of AttachmentsImpl. + * + * @param withRetrofit the Retrofit instance built from a Retrofit Builder. + * @param withClient the instance of the service client containing this + * operation class. + */ + RestAttachments(Retrofit withRetrofit, RestConnectorClient withClient) { + this.service = withRetrofit.create(AttachmentsService.class); + this.client = withClient; + } + + /** + * The interface defining all the services for Attachments to be used by + * Retrofit to perform actually REST calls. + */ + @SuppressWarnings({ "checkstyle:linelength", "checkstyle:JavadocMethod" }) + interface AttachmentsService { + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Attachments getAttachmentInfo" }) + @GET("v3/attachments/{attachmentId}") + CompletableFuture> getAttachmentInfo( + @Path("attachmentId") String attachmentId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Attachments getAttachment" }) + @GET("v3/attachments/{attachmentId}/views/{viewId}") + @Streaming + CompletableFuture> getAttachment( + @Path("attachmentId") String attachmentId, + @Path("viewId") String viewId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + } + + /** + * GetAttachmentInfo. Get AttachmentInfo structure describing the attachment + * views. + * + * @param attachmentId attachment id + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the AttachmentInfo object + */ + public CompletableFuture getAttachmentInfo(String attachmentId) { + if (attachmentId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter attachmentId is required and cannot be null." + )); + } + + return service.getAttachmentInfo( + attachmentId, this.client.getAcceptLanguage(), this.client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getAttachmentInfoDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getAttachmentInfo", responseBodyResponse); + } + }); + } + + private ServiceResponse getAttachmentInfoDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * GetAttachment. Get the named view as binary content. + * + * @param attachmentId attachment id + * @param viewId View id from attachmentInfo + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the InputStream object + */ + public CompletableFuture getAttachment(String attachmentId, String viewId) { + if (attachmentId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter attachmentId is required and cannot be null." + )); + } + if (viewId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter viewId is required and cannot be null." + )); + } + + return service.getAttachment( + attachmentId, viewId, this.client.getAcceptLanguage(), this.client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getAttachmentDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getAttachment", responseBodyResponse); + } + }); + } + + private ServiceResponse getAttachmentDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_MOVED_PERM, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_MOVED_TEMP, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Get the URI of an attachment view. + * @param attachmentId id of the attachment. + * @param viewId default is "original". + * @return URI of the attachment. + */ + @Override + public String getAttachmentUri(String attachmentId, String viewId) { + if (StringUtils.isEmpty(attachmentId)) { + throw new IllegalArgumentException("Must provide an attachmentId"); + } + + if (StringUtils.isEmpty(viewId)) { + viewId = "original"; + } + + String baseUrl = client.baseUrl(); + String uri = baseUrl + + (baseUrl.endsWith("/") ? "" : "/") + + "v3/attachments/{attachmentId}/views/{viewId}"; + uri = uri.replace("{attachmentId}", URLEncoder.encode(attachmentId)); + uri = uri.replace("{viewId}", URLEncoder.encode(viewId)); + + return uri; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestBotSignIn.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestBotSignIn.java new file mode 100644 index 000000000..2f19d4145 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestBotSignIn.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ +package com.microsoft.bot.connector.rest; + +import com.google.common.reflect.TypeToken; +import com.microsoft.bot.connector.Async; +import retrofit2.Retrofit; +import com.microsoft.bot.connector.BotSignIn; +import com.microsoft.bot.restclient.ServiceResponse; +import com.microsoft.bot.schema.SignInResource; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.concurrent.CompletableFuture; +import okhttp3.ResponseBody; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.Query; +import retrofit2.Response; +/** + * An instance of this class provides access to all the operations defined in + * BotSignIns. + */ +@SuppressWarnings("PMD") +public class RestBotSignIn implements BotSignIn { + /** The Retrofit service to perform REST calls. */ + private BotSignInsService service; + /** The service client containing this operation class. */ + private RestOAuthClient client; + /** + * Initializes an instance of BotSignInsImpl. + * + * @param withRetrofit the Retrofit instance built from a Retrofit Builder. + * @param withClient the instance of the service client containing this + * operation class. + */ + public RestBotSignIn(Retrofit withRetrofit, RestOAuthClient withClient) { + this.service = withRetrofit.create(BotSignInsService.class); + this.client = withClient; + } + /** + * The interface defining all the services for BotSignIns to be used by Retrofit + * to perform actually REST calls. + */ + @SuppressWarnings({"checkstyle:linelength", "checkstyle:JavadocMethod"}) + interface BotSignInsService { + @Headers({"Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.BotSignIns getSignInUrl"}) + @GET("api/botsignin/GetSignInUrl") + CompletableFuture> getSignInUrl( + @Query("state") String state, + @Query("code_challenge") String codeChallenge, + @Query("emulatorUrl") String emulatorUrl, + @Query("finalRedirect") String finalRedirect + ); + @Headers({"Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.BotSignIns GetSignInResource"}) + @GET("api/botsignin/GetSignInResource") + CompletableFuture> getSignInResource( + @Query("state") String state, + @Query("code_challenge") String codeChallenge, + @Query("emulatorUrl") String emulatorUrl, + @Query("finalRedirect") String finalRedirect + ); + } + /** + * + * @param state the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + public CompletableFuture getSignInUrl(String state) { + if (state == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter state is required and cannot be null." + )); + } + final String codeChallenge = null; + final String emulatorUrl = null; + final String finalRedirect = null; + return service.getSignInUrl(state, codeChallenge, emulatorUrl, finalRedirect) + .thenApply(responseBodyResponse -> { + try { + return getSignInUrlDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getSignInUrl", responseBodyResponse); + } + }); + } + /** + * + * @param state the String value + * @param codeChallenge the String value + * @param emulatorUrl the String value + * @param finalRedirect the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + public CompletableFuture getSignInUrl( + String state, + String codeChallenge, + String emulatorUrl, + String finalRedirect + ) { + if (state == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter state is required and cannot be null." + )); + } + return service.getSignInUrl(state, codeChallenge, emulatorUrl, finalRedirect) + .thenApply(responseBodyResponse -> { + try { + return getSignInUrlDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getSignInUrl", responseBodyResponse); + } + }); + } + private ServiceResponse getSignInUrlDelegate( + Response response + ) throws ErrorResponseException, IllegalArgumentException { + if (!response.isSuccessful()) { + throw new ErrorResponseException("getSignInUrl", response); + } + return new ServiceResponse<>(response.body().source().buffer().readUtf8(), response); + } + /** + * + * @param state the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + public CompletableFuture getSignInResource(String state) { + if (state == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter state is required and cannot be null." + )); + } + final String codeChallenge = null; + final String emulatorUrl = null; + final String finalRedirect = null; + return service.getSignInResource(state, codeChallenge, emulatorUrl, finalRedirect) + .thenApply(responseBodyResponse -> { + try { + return getSignInResourceDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getSignInResource", responseBodyResponse); + } + }); + } + /** + * + * @param state the String value + * @param codeChallenge the String value + * @param emulatorUrl the String value + * @param finalRedirect the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the String object + */ + public CompletableFuture getSignInResource( + String state, + String codeChallenge, + String emulatorUrl, + String finalRedirect + ) { + if (state == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter state is required and cannot be null." + )); + } + return service.getSignInResource(state, codeChallenge, emulatorUrl, finalRedirect) + .thenApply(responseBodyResponse -> { + try { + return getSignInResourceDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getSignInResource", responseBodyResponse); + } + }); + } + private ServiceResponse getSignInResourceDelegate( + Response response + ) throws ErrorResponseException, IllegalArgumentException, IOException { + if (!response.isSuccessful()) { + throw new ErrorResponseException("getSignInResource", response); + } + return this.client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_MOVED_PERM, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_MOVED_TEMP, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java new file mode 100644 index 000000000..831e126f7 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConnectorClient.java @@ -0,0 +1,295 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.connector.Attachments; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.UserAgent; +import com.microsoft.bot.restclient.ServiceClient; +import com.microsoft.bot.restclient.ServiceResponseBuilder; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.retry.RetryStrategy; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * The Bot Connector REST API allows your bot to send and receive messages to + * channels configured in the [Bot Framework Developer + * Portal](https://dev.botframework.com). The Connector service uses + * industry-standard REST and JSON over HTTPS. + * + * Client libraries for this REST API are available. See below for a list. + * + * Many bots will use both the Bot Connector REST API and the associated [Bot + * State REST API](/en-us/restapi/state). The Bot State REST API allows a bot to + * store and retrieve state associated with users and conversations. + * + * Authentication for both the Bot Connector and Bot State REST APIs is + * accomplished with JWT Bearer tokens, and is described in detail in the + * [Connector Authentication](/en-us/restapi/authentication) document. + */ +public class RestConnectorClient extends ServiceClient implements ConnectorClient { + private static final int RETRY_TIMEOUT = 30; + + /** + * Initializes an instance of ConnectorClient client. + * + * @param credentials the management credentials for Azure + */ + public RestConnectorClient(ServiceClientCredentials credentials) { + this("https://api.botframework.com", credentials); + } + + /** + * Initializes an instance of ConnectorClient client. + * + * @param baseUrl the base URL of the host + * @param credentials the management credentials for Azure + */ + public RestConnectorClient(String baseUrl, ServiceClientCredentials credentials) { + super(baseUrl, credentials); + initialize(); + } + + /** + * Initializes an instance of ConnectorClient client. + * + * @param restClient the REST client to connect to Azure. + */ + public RestConnectorClient(RestClient restClient) { + super(restClient); + initialize(); + } + + /** + * Initialize the object post-construction. + */ + protected void initialize() { + this.acceptLanguage = "en-US"; + this.longRunningOperationRetryTimeout = RETRY_TIMEOUT; + this.generateClientRequestId = true; + this.attachments = new RestAttachments(restClient().retrofit(), this); + this.conversations = new RestConversations(restClient().retrofit(), this); + this.userAgentString = UserAgent.value(); + + // this.restClient().withLogLevel(LogLevel.BODY_AND_HEADERS); + } + + /** + * Gets the REST client. + * + * @return the {@link RestClient} object. + */ + @Override + public RestClient getRestClient() { + return super.restClient(); + } + + /** + * Returns the base url for this ConnectorClient. + * + * @return The base url. + */ + @Override + public String baseUrl() { + return getRestClient().retrofit().baseUrl().toString(); + } + + /** + * Returns the credentials in use. + * + * @return The ServiceClientCredentials in use. + */ + public ServiceClientCredentials credentials() { + return getRestClient().credentials(); + } + + /** Gets or sets the preferred language for the response. */ + private String acceptLanguage; + private String userAgentString; + + /** + * Gets the preferred language for the response.. + * + * @return the acceptLanguage value. + */ + @Override + public String getAcceptLanguage() { + return this.acceptLanguage; + } + + /** + * Sets the preferred language for the response.. + * + * @param withAcceptLanguage the acceptLanguage value. + */ + @Override + public void setAcceptLanguage(String withAcceptLanguage) { + this.acceptLanguage = withAcceptLanguage; + } + + private RetryStrategy retryStrategy = null; + + /** + * Sets the Rest retry strategy. + * + * @param strategy The {@link RetryStrategy} to use. + */ + public void setRestRetryStrategy(RetryStrategy strategy) { + this.retryStrategy = strategy; + } + + /** + * Gets the Rest retry strategy. + * + * @return The {@link RetryStrategy} being used. + */ + public RetryStrategy getRestRetryStrategy() { + return this.retryStrategy; + } + + /** + * Gets or sets the retry timeout in seconds for Long Running Operations. + * Default value is 30. + */ + private int longRunningOperationRetryTimeout; + + /** + * Gets the retry timeout in seconds for Long Running Operations. Default value + * is 30. + * + * @return the timeout value. + */ + @Override + public int getLongRunningOperationRetryTimeout() { + return this.longRunningOperationRetryTimeout; + } + + /** + * Sets the retry timeout in seconds for Long Running Operations. Default value + * is 30. + * + * @param timeout the longRunningOperationRetryTimeout value. + */ + @Override + public void setLongRunningOperationRetryTimeout(int timeout) { + this.longRunningOperationRetryTimeout = timeout; + } + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. + */ + private boolean generateClientRequestId; + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. + * + * @return the generateClientRequestId value. + */ + @Override + public boolean getGenerateClientRequestId() { + return this.generateClientRequestId; + } + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. + * + * @param requestId the generateClientRequestId value. + */ + @Override + public void setGenerateClientRequestId(boolean requestId) { + this.generateClientRequestId = requestId; + } + + /** + * The Attachments object to access its operations. + */ + private Attachments attachments; + + /** + * Gets the Attachments object to access its operations. + * + * @return the Attachments object. + */ + @Override + public Attachments getAttachments() { + return this.attachments; + } + + /** + * The Conversations object to access its operations. + */ + private Conversations conversations; + + /** + * Gets the Conversations object to access its operations. + * + * @return the Conversations object. + */ + @Override + public Conversations getConversations() { + return this.conversations; + } + + /** + * Gets the User-Agent header for the client. + * + * @return the user agent string. + */ + + @Override + public String getUserAgent() { + return this.userAgentString; + } + + /** + * This is to override the AzureServiceClient version. + * + * @return The user agent. Same as {@link #getUserAgent()} + */ + @Override + public String userAgent() { + return getUserAgent(); + } + + /** + * This is a copy of what the Azure Client does to create a RestClient. This + * returns a RestClient.Builder so that the app can create a custom RestClient, + * and supply it to ConnectorClient during construction. + * + * One use case of this is for supplying a Proxy to the RestClient. Though it is + * recommended to set proxy information via the Java system properties. + * + * @param baseUrl Service endpoint + * @param credentials auth credentials. + * @return A RestClient.Builder. + */ + public static RestClient.Builder getDefaultRestClientBuilder( + String baseUrl, + ServiceClientCredentials credentials + ) { + return new RestClient.Builder(new OkHttpClient.Builder(), new Retrofit.Builder()) + .withBaseUrl(baseUrl) + .withCredentials(credentials) + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()); + } + + /** + * AutoDisposable close. + */ + @Override + public void close() { + + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConversations.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConversations.java new file mode 100644 index 000000000..82bce2eed --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestConversations.java @@ -0,0 +1,948 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.ConversationConstants; +import com.microsoft.bot.restclient.ServiceResponseBuilder; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.AttachmentData; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.PagedMembersResult; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Transcript; +import retrofit2.Retrofit; +import com.microsoft.bot.connector.Conversations; +import com.google.common.reflect.TypeToken; +import com.microsoft.bot.restclient.ServiceResponse; +import com.microsoft.bot.restclient.Validator; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import okhttp3.ResponseBody; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.HTTP; +import retrofit2.http.Path; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Query; +import retrofit2.Response; + +/** + * An instance of this class provides access to all the operations defined in + * Conversations. + */ +public class RestConversations implements Conversations { + /** + * The Retrofit service to perform REST calls. + */ + private ConversationsService service; + /** + * The service client containing this operation class. + */ + private RestConnectorClient client; + + /** + * Initializes an instance of ConversationsImpl. + * + * @param withRetrofit the Retrofit instance built from a Retrofit Builder. + * @param withClient the instance of the service client containing this + * operation class. + */ + RestConversations(Retrofit withRetrofit, RestConnectorClient withClient) { + this.service = withRetrofit.create(ConversationsService.class); + client = withClient; + } + + /** + * The interface defining all the services for Conversations to be used by + * Retrofit to perform actually REST calls. + */ + @SuppressWarnings({ "checkstyle:linelength", "checkstyle:JavadocMethod" }) + interface ConversationsService { + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversations" }) + @GET("v3/conversations") + CompletableFuture> getConversations( + @Query("continuationToken") String continuationToken, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations createConversation" }) + @POST("v3/conversations") + CompletableFuture> createConversation( + @Body ConversationParameters parameters, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations sendToConversation" }) + @POST("v3/conversations/{conversationId}/activities") + CompletableFuture> sendToConversation( + @Path("conversationId") String conversationId, + @Body Activity activity, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations updateActivity" }) + @PUT("v3/conversations/{conversationId}/activities/{activityId}") + CompletableFuture> updateActivity( + @Path("conversationId") String conversationId, + @Path("activityId") String activityId, + @Body Activity activity, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ + "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations replyToActivity" + }) + @POST("v3/conversations/{conversationId}/activities/{activityId}") + CompletableFuture> replyToActivity( + @Path("conversationId") String conversationId, + @Path("activityId") String activityId, + @Body Activity activity, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent, + @Header(ConversationConstants.CONVERSATION_ID_HTTP_HEADERNAME) String conversationIdHeader + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations deleteActivity" }) + @HTTP(path = "v3/conversations/{conversationId}/activities/{activityId}", method = "DELETE", hasBody = true) + CompletableFuture> deleteActivity( + @Path("conversationId") String conversationId, + @Path("activityId") String activityId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversationMembers" }) + @GET("v3/conversations/{conversationId}/members") + CompletableFuture> getConversationMembers( + @Path("conversationId") String conversationId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversationMembers" }) + @GET("v3/conversations/{conversationId}/members/{userId}") + CompletableFuture> getConversationMember( + @Path("userId") String userId, + @Path("conversationId") String conversationId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations deleteConversationMember" }) + @HTTP(path = "v3/conversations/{conversationId}/members/{memberId}", method = "DELETE", hasBody = true) + CompletableFuture> deleteConversationMember( + @Path("conversationId") String conversationId, + @Path("memberId") String memberId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations getActivityMembers" }) + @GET("v3/conversations/{conversationId}/activities/{activityId}/members") + CompletableFuture> getActivityMembers( + @Path("conversationId") String conversationId, + @Path("activityId") String activityId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations uploadAttachment" }) + @POST("v3/conversations/{conversationId}/attachments") + CompletableFuture> uploadAttachment( + @Path("conversationId") String conversationId, + @Body AttachmentData attachmentUpload, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations sendConversationHistory" }) + @POST("v3/conversations/{conversationId}/activities/history") + CompletableFuture> sendConversationHistory( + @Path("conversationId") String conversationId, + @Body Transcript history, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversationPagedMembers" }) + @GET("v3/conversations/{conversationId}/pagedmembers") + CompletableFuture> getConversationPagedMembers( + @Path("conversationId") String conversationId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Conversations getConversationPagedMembers" }) + @GET("v3/conversations/{conversationId}/pagedmembers?continuationToken={continuationToken}") + CompletableFuture> getConversationPagedMembers( + @Path("conversationId") String conversationId, + @Path("continuationToken") String continuationToken, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + } + + /** + * Implementation of getConversations. + * + * @see Conversations#getConversations + */ + @Override + public CompletableFuture getConversations() { + return getConversations(null); + } + + /** + * Implementation of getConversations. + * + * @see Conversations#getConversations + */ + @Override + public CompletableFuture getConversations(String continuationToken) { + return service + .getConversations(continuationToken, client.getAcceptLanguage(), client.getUserAgent()) + .thenApply(responseBodyResponse -> { + try { + return getConversationsDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getConversations", responseBodyResponse); + } + }); + } + + private ServiceResponse getConversationsDelegate( + Response response + ) throws ErrorResponseException, IOException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of createConversation. + * + * @see Conversations#createConversation + */ + @Override + public CompletableFuture createConversation( + ConversationParameters parameters + ) { + if (parameters == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter parameters is required and cannot be null." + )); + } + Validator.validate(parameters); + + return service + .createConversation(parameters, client.getAcceptLanguage(), client.getUserAgent()) + .thenApply(responseBodyResponse -> { + try { + return createConversationDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "createConversation", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse createConversationDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance( + client.serializerAdapter() + ) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register( + HttpURLConnection.HTTP_CREATED, new TypeToken() { + }.getType() + ) + .register( + HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType() + ) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of sendToConversation. + * + * @see Conversations#sendToConversation + */ + @Override + public CompletableFuture sendToConversation( + String conversationId, + Activity activity + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activity is required and cannot be null." + )); + } + Validator.validate(activity); + + return service.sendToConversation( + conversationId, activity, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return sendToConversationDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("sendToConversation", responseBodyResponse); + } + }); + } + + private ServiceResponse sendToConversationDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_CREATED, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of updateActivity. + * + * @see Conversations#updateActivity + */ + @Override + public CompletableFuture updateActivity( + String conversationId, + String activityId, + Activity activity + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (activityId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activityId is required and cannot be null." + )); + } + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activity is required and cannot be null." + )); + } + + return Async.tryCompletable(() -> { + Validator.validate(activity); + return service.updateActivity( + conversationId, activityId, activity, client.getAcceptLanguage(), + client.getUserAgent() + ) + + .thenApply(responseBodyResponse -> { + try { + return updateActivityDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "updateActivity", responseBodyResponse); + } + }); + }); + } + + private ServiceResponse updateActivityDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_CREATED, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of replyToActivity. + * + * @see Conversations#replyToActivity + */ + @Override + public CompletableFuture replyToActivity( + String conversationId, + String activityId, + Activity activity + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (activityId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activityId is required and cannot be null." + )); + } + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activity is required and cannot be null." + )); + } + Validator.validate(activity); + + return service.replyToActivity( + conversationId, + activityId, + activity, + client.getAcceptLanguage(), + client.getUserAgent(), + conversationId + ) + + .thenApply(responseBodyResponse -> { + try { + return replyToActivityDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("replyToActivity", responseBodyResponse); + } + }); + } + + private ServiceResponse replyToActivityDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_CREATED, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of deleteActivity. + * + * @see Conversations#deleteActivity + */ + @Override + public CompletableFuture deleteActivity(String conversationId, String activityId) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (activityId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activityId is required and cannot be null." + )); + } + + return service.deleteActivity( + conversationId, activityId, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return deleteActivityDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("deleteActivity", responseBodyResponse); + } + }); + } + + private ServiceResponse deleteActivityDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of getConversationMembers. + * + * @see Conversations#getConversationMembers + */ + @Override + public CompletableFuture> getConversationMembers(String conversationId) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + + return service.getConversationMembers( + conversationId, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getConversationMembersDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "getConversationMembers", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse> getConversationMembersDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + ., ErrorResponseException>newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken>() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of getConversationMember. + * + * @see Conversations#getConversationMember + */ + @Override + public CompletableFuture getConversationMember( + String userId, + String conversationId + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + + return service.getConversationMember( + userId, conversationId, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getConversationMemberDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "getConversationMember", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse getConversationMemberDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return ((ServiceResponseBuilder) client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class)) + .withThrowOnGet404(true) + .build(response); + } + + /** + * Implementation of deleteConversationMember. + * + * @see Conversations#deleteConversationMember + */ + @Override + public CompletableFuture deleteConversationMember( + String conversationId, + String memberId + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (memberId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter memberId is required and cannot be null." + )); + } + + return service.deleteConversationMember( + conversationId, memberId, client.getAcceptLanguage(), client.getUserAgent() + ) + + .thenApply(responseBodyResponse -> { + try { + return deleteConversationMemberDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "deleteConversationMember", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse deleteConversationMemberDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_NO_CONTENT, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of getActivityMembers. + * + * @see Conversations#getActivityMembers + */ + @Override + public CompletableFuture> getActivityMembers( + String conversationId, + String activityId + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (activityId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter activityId is required and cannot be null." + )); + } + + return service.getActivityMembers( + conversationId, activityId, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getActivityMembersDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getActivityMembers", responseBodyResponse); + } + }); + } + + private ServiceResponse> getActivityMembersDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + ., ErrorResponseException>newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken>() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of uploadAttachment. + * + * @see Conversations#uploadAttachment + */ + @Override + public CompletableFuture uploadAttachment( + String conversationId, + AttachmentData attachmentUpload + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (attachmentUpload == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter attachmentUpload is required and cannot be null." + )); + } + Validator.validate(attachmentUpload); + + return service.uploadAttachment( + conversationId, attachmentUpload, client.getAcceptLanguage(), client.getUserAgent() + ) + + .thenApply(responseBodyResponse -> { + try { + return uploadAttachmentDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("uploadAttachment", responseBodyResponse); + } + }); + } + + private ServiceResponse uploadAttachmentDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_CREATED, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of sendConversationHistory. + * + * @see Conversations#sendConversationHistory + */ + @Override + public CompletableFuture sendConversationHistory( + String conversationId, + Transcript history + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (history == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter history is required and cannot be null." + )); + } + Validator.validate(history); + + return service.sendConversationHistory( + conversationId, history, client.getAcceptLanguage(), client.getUserAgent() + ) + + .thenApply(responseBodyResponse -> { + try { + return sendConversationHistoryDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "sendConversationHistory", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse sendConversationHistoryDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_CREATED, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of getConversationPagedMembers. + * + * @see Conversations#getConversationPagedMembers(String conversationId) + */ + @Override + public CompletableFuture getConversationPagedMembers( + String conversationId + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + + return service.getConversationPagedMembers( + conversationId, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getConversationPagedMembersDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "getConversationPagedMembers", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse getConversationPagedMembersDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of getConversationPagedMembers. + * + * @see Conversations#getConversationPagedMembers(String conversationId, String + * continuationToken) + * + * @param conversationId Conversation ID + * @param continuationToken The continuationToken from a previous call. + * @throws IllegalArgumentException thrown if parameters fail the validation + * @throws RuntimeException all other wrapped checked exceptions if the + * request fails to be sent + * @return the PagedMembersResult object if successful. + */ + public CompletableFuture getConversationPagedMembers( + String conversationId, + String continuationToken + ) { + if (conversationId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter conversationId is required and cannot be null." + )); + } + if (continuationToken == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter continuationToken is required and cannot be null." + )); + } + + return service.getConversationPagedMembers( + conversationId, continuationToken, client.getAcceptLanguage(), client.getUserAgent() + ).thenApply(responseBodyResponse -> { + try { + return getConversationPagedMembers2Delegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException( + "getConversationPagedMembers", + responseBodyResponse + ); + } + }); + } + + private ServiceResponse getConversationPagedMembers2Delegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestOAuthClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestOAuthClient.java new file mode 100644 index 000000000..f4d6e673a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestOAuthClient.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.connector.BotSignIn; +import com.microsoft.bot.connector.OAuthClient; +import com.microsoft.bot.connector.UserToken; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.ServiceClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; + +/** + * Rest OAuth client. + */ +public class RestOAuthClient extends ServiceClient implements OAuthClient { + /** + * The BotSignIns object to access its operations. + */ + private BotSignIn botSignIn; + + /** + * The UserTokens object to access its operations. + */ + private UserToken userToken; + + /** + * Initializes an instance of ConnectorClient client. + * + * @param restClient The RestClient to use. + */ + public RestOAuthClient(RestClient restClient) { + super(restClient); + initialize(); + } + + /** + * Initializes an instance of ConnectorClient client. + * + * @param baseUrl the base URL of the host + * @param credentials the management credentials for Azure + */ + public RestOAuthClient(String baseUrl, ServiceClientCredentials credentials) { + super(baseUrl, credentials); + initialize(); + } + + /** + * Gets the BotSignIns object to access its operations. + * + * @return the BotSignIns object. + */ + @Override + public BotSignIn getBotSignIn() { + return botSignIn; + } + + /** + * Gets the UserTokens object to access its operations. + * + * @return the UserTokens object. + */ + @Override + public UserToken getUserToken() { + return userToken; + } + + /** + * Post construction initialization. + */ + protected void initialize() { + botSignIn = new RestBotSignIn(restClient().retrofit(), this); + userToken = new RestUserToken(restClient().retrofit(), this); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestTeamsConnectorClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestTeamsConnectorClient.java new file mode 100644 index 000000000..040ada972 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestTeamsConnectorClient.java @@ -0,0 +1,273 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.connector.UserAgent; +import com.microsoft.bot.connector.teams.TeamsConnectorClient; +import com.microsoft.bot.connector.teams.TeamsOperations; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.ServiceClient; +import com.microsoft.bot.restclient.ServiceResponseBuilder; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.retry.RetryStrategy; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * The Bot Connector REST API allows your bot to send and receive messages to + * channels configured in the [Bot Framework Developer + * Portal](https://dev.botframework.com). The Connector service uses + * industry-standard REST and JSON over HTTPS. + * + * Client libraries for this REST API are available. See below for a list. + * + * Many bots will use both the Bot Connector REST API and the associated [Bot + * State REST API](/en-us/restapi/state). The Bot State REST API allows a bot to + * store and retrieve state associated with Teams. + * + * Authentication for both the Bot Connector and Bot State REST APIs is + * accomplished with JWT Bearer tokens, and is described in detail in the + * [Connector Authentication](/en-us/restapi/authentication) document. + */ +public class RestTeamsConnectorClient extends ServiceClient implements TeamsConnectorClient { + private static final int RETRY_TIMEOUT = 30; + + /** Gets or sets the preferred language for the response. */ + private String acceptLanguage; + private String userAgentString; + + private RetryStrategy retryStrategy = null; + + private TeamsOperations teamsOperations = null; + + /** + * Initializes an instance of TeamsConnectorClient client. + * + * @param credentials the management credentials for Azure + */ + public RestTeamsConnectorClient(ServiceClientCredentials credentials) { + this("https://api.botframework.com", credentials); + } + + /** + * Initializes an instance of TeamsConnectorClient client. + * + * @param baseUrl the base URL of the host + * @param credentials the management credentials for Azure + */ + public RestTeamsConnectorClient(String baseUrl, ServiceClientCredentials credentials) { + super(baseUrl, credentials); + initialize(); + } + + /** + * Initializes an instance of TeamsConnectorClient client. + * + * @param restClient the REST client to connect to Azure. + */ + protected RestTeamsConnectorClient(RestClient restClient) { + super(restClient); + initialize(); + } + + /** + * Initialize the object post-construction. + */ + protected void initialize() { + this.acceptLanguage = "en-US"; + this.longRunningOperationRetryTimeout = RETRY_TIMEOUT; + this.generateClientRequestId = true; + this.teamsOperations = new RestTeamsOperations(restClient().retrofit(), this); + this.userAgentString = UserAgent.value(); + + // this.restClient().withLogLevel(LogLevel.BODY_AND_HEADERS); + } + + /** + * Gets the REST client. + * + * @return the {@link RestClient} object. + */ + @Override + public RestClient getRestClient() { + return super.restClient(); + } + + /** + * Returns the base url for this ConnectorClient. + * + * @return The base url. + */ + @Override + public String baseUrl() { + return getRestClient().retrofit().baseUrl().toString(); + } + + /** + * Returns the credentials in use. + * + * @return The ServiceClientCredentials in use. + */ + public ServiceClientCredentials credentials() { + return getRestClient().credentials(); + } + + /** + * Gets the preferred language for the response.. + * + * @return the acceptLanguage value. + */ + @Override + public String getAcceptLanguage() { + return this.acceptLanguage; + } + + /** + * Sets the preferred language for the response.. + * + * @param withAcceptLanguage the acceptLanguage value. + */ + public void setAcceptLanguage(String withAcceptLanguage) { + this.acceptLanguage = withAcceptLanguage; + } + + /** + * Gets the User-Agent header for the client. + * + * @return the user agent string. + */ + @Override + public String getUserAgent() { + return this.userAgentString; + } + + /** + * This is to override the AzureServiceClient version. + * + * @return The user agent. Same as {@link #getUserAgent()} + */ + @Override + public String userAgent() { + return getUserAgent(); + } + + /** + * Sets the Rest retry strategy. + * + * @param strategy The {@link RetryStrategy} to use. + */ + public void setRestRetryStrategy(RetryStrategy strategy) { + this.retryStrategy = strategy; + } + + /** + * Gets the Rest retry strategy. + * + * @return The {@link RetryStrategy} being used. + */ + public RetryStrategy getRestRetryStrategy() { + return this.retryStrategy; + } + + /** + * Gets or sets the retry timeout in seconds for Long Running Operations. + * Default value is 30. + */ + private int longRunningOperationRetryTimeout; + + /** + * Gets the retry timeout in seconds for Long Running Operations. Default value + * is 30. + * + * @return the timeout value. + */ + @Override + public int getLongRunningOperationRetryTimeout() { + return this.longRunningOperationRetryTimeout; + } + + /** + * Sets the retry timeout in seconds for Long Running Operations. Default value + * is 30. + * + * @param timeout the longRunningOperationRetryTimeout value. + */ + @Override + public void setLongRunningOperationRetryTimeout(int timeout) { + this.longRunningOperationRetryTimeout = timeout; + } + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. + */ + private boolean generateClientRequestId; + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. + * + * @return the generateClientRequestId value. + */ + @Override + public boolean getGenerateClientRequestId() { + return this.generateClientRequestId; + } + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. + * + * @param requestId the generateClientRequestId value. + */ + @Override + public void setGenerateClientRequestId(boolean requestId) { + this.generateClientRequestId = requestId; + } + + /** + * Returns an instance of TeamsOperations. + * + * @return A TeamsOperations instance. + */ + @Override + public TeamsOperations getTeams() { + return teamsOperations; + } + + /** + * This is a copy of what the Azure Client does to create a RestClient. This + * returns a RestClient.Builder so that the app can create a custom RestClient, + * and supply it to ConnectorClient during construction. + * + * One use case of this is for supplying a Proxy to the RestClient. Though it is + * recommended to set proxy information via the Java system properties. + * + * @param baseUrl Service endpoint + * @param credentials auth credentials. + * @return A RestClient.Builder. + */ + public static RestClient.Builder getDefaultRestClientBuilder( + String baseUrl, + ServiceClientCredentials credentials + ) { + return new RestClient.Builder(new OkHttpClient.Builder(), new Retrofit.Builder()) + .withBaseUrl(baseUrl) + .withCredentials(credentials) + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()); + } + + /** + * AutoDisposable close. + */ + @Override + public void close() throws Exception { + + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestTeamsOperations.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestTeamsOperations.java new file mode 100644 index 000000000..4c6c7caed --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestTeamsOperations.java @@ -0,0 +1,243 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.google.common.reflect.TypeToken; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.teams.TeamsOperations; +import com.microsoft.bot.restclient.ServiceResponse; +import com.microsoft.bot.schema.teams.ConversationList; +import com.microsoft.bot.schema.teams.MeetingInfo; +import com.microsoft.bot.schema.teams.TeamDetails; +import com.microsoft.bot.schema.teams.TeamsMeetingParticipant; +import okhttp3.ResponseBody; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.Path; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.concurrent.CompletableFuture; +import retrofit2.http.Query; + +/** + * msrest impl of TeamsOperations. + */ +public class RestTeamsOperations implements TeamsOperations { + /** The Retrofit service to perform REST calls. */ + private TeamsService service; + + /** The service client containing this operation class. */ + private RestTeamsConnectorClient client; + + /** + * Initializes an instance of ConversationsImpl. + * + * @param withRetrofit the Retrofit instance built from a Retrofit Builder. + * @param withClient the instance of the service client containing this + * operation class. + */ + RestTeamsOperations(Retrofit withRetrofit, RestTeamsConnectorClient withClient) { + service = withRetrofit.create(RestTeamsOperations.TeamsService.class); + client = withClient; + } + + /** + * Implementation of fetchChannelList. + * + * @see TeamsOperations#fetchChannelList + */ + @Override + public CompletableFuture fetchChannelList(String teamId) { + if (teamId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter teamId is required and cannot be null." + )); + } + + return service.fetchChannelList(teamId, client.getAcceptLanguage(), client.getUserAgent()) + .thenApply(responseBodyResponse -> { + try { + return fetchChannelListDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("fetchChannelList", responseBodyResponse); + } + }); + } + + private ServiceResponse fetchChannelListDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Implementation of fetchTeamDetails. + * + * @see TeamsOperations#fetchTeamDetails + */ + @Override + public CompletableFuture fetchTeamDetails(String teamId) { + if (teamId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter teamId is required and cannot be null." + )); + } + + return service.fetchTeamDetails(teamId, client.getAcceptLanguage(), client.getUserAgent()) + .thenApply(responseBodyResponse -> { + try { + return fetchTeamDetailsDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("fetchTeamDetails", responseBodyResponse); + } + }); + } + + private ServiceResponse fetchTeamDetailsDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Fetches Teams meeting participant details. + * @param meetingId Teams meeting id + * @param participantId Teams meeting participant id + * @param tenantId Teams meeting tenant id + * @return TeamsParticipantChannelAccount + */ + public CompletableFuture fetchParticipant( + String meetingId, + String participantId, + String tenantId + ) { + return service.fetchParticipant( + meetingId, participantId, tenantId, client.getAcceptLanguage(), client.getUserAgent() + ) + .thenApply(responseBodyResponse -> { + try { + return fetchParticipantDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("fetchParticipant", responseBodyResponse); + } + }); + } + + private ServiceResponse fetchParticipantDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Fetches Teams meeting participant details. + * @param meetingId Teams meeting id + * @return TeamsParticipantChannelAccount + */ + @Override + public CompletableFuture fetchMeetingInfo(String meetingId) { + return service.fetchMeetingInfo( + meetingId, client.getAcceptLanguage(), client.getUserAgent() + ) + .thenApply(responseBodyResponse -> { + try { + return fetchMeetingInfoDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("fetchMeetingInfo", responseBodyResponse); + } + }); + } + + private ServiceResponse fetchMeetingInfoDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * The interface defining all the services for TeamsOperations to be used by + * Retrofit to perform actually REST calls. + */ + @SuppressWarnings({ "checkstyle:linelength", "checkstyle:JavadocMethod" }) + interface TeamsService { + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Teams fetchChannelList" }) + @GET("v3/teams/{teamId}/conversations") + CompletableFuture> fetchChannelList( + @Path("teamId") String teamId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Teams fetchTeamDetails" }) + @GET("v3/teams/{teamId}") + CompletableFuture> fetchTeamDetails( + @Path("teamId") String teamId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Teams fetchParticipant" }) + @GET("v1/meetings/{meetingId}/participants/{participantId}") + CompletableFuture> fetchParticipant( + @Path("meetingId") String meetingId, + @Path("participantId") String participantId, + @Query("tenantId") String tenantId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.Teams fetchMeetingInfo" }) + @GET("v1/meetings/{meetingId}") + CompletableFuture> fetchMeetingInfo( + @Path("meetingId") String meetingId, + @Header("accept-language") String acceptLanguage, + @Header("User-Agent") String userAgent + ); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestUserToken.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestUserToken.java new file mode 100644 index 000000000..24e23b627 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/RestUserToken.java @@ -0,0 +1,577 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.connector.rest; + +import com.microsoft.bot.connector.Async; +import retrofit2.Retrofit; +import com.microsoft.bot.connector.UserToken; +import com.google.common.reflect.TypeToken; +import com.microsoft.bot.schema.AadResourceUrls; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; +import com.microsoft.bot.restclient.ServiceResponse; +import com.microsoft.bot.restclient.Validator; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import okhttp3.ResponseBody; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.HTTP; +import retrofit2.http.POST; +import retrofit2.http.Query; +import retrofit2.Response; + +/** + * An instance of this class provides access to all the operations defined in + * UserTokens. + */ +public class RestUserToken implements UserToken { + /** The Retrofit service to perform REST calls. */ + private UserTokensService service; + /** The service client containing this operation class. */ + private RestOAuthClient client; + + /** + * Initializes an instance of UserTokensImpl. + * + * @param withRetrofit the Retrofit instance built from a Retrofit Builder. + * @param withClient the instance of the service client containing this + * operation class. + */ + public RestUserToken(Retrofit withRetrofit, RestOAuthClient withClient) { + this.service = withRetrofit.create(UserTokensService.class); + this.client = withClient; + } + + /** + * The interface defining all the services for UserTokens to be used by Retrofit + * to perform actually REST calls. + */ + @SuppressWarnings({ "checkstyle:linelength", "checkstyle:JavadocMethod" }) + interface UserTokensService { + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens getToken" }) + @GET("api/usertoken/GetToken") + CompletableFuture> getToken( + @Query("userId") String userId, + @Query("connectionName") String connectionName, + @Query("channelId") String channelId, + @Query("code") String code + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens exchangeToken" }) + @POST("api/usertoken/Exchange") + CompletableFuture> exchangeToken( + @Query("userId") String userId, + @Query("connectionName") String connectionName, + @Query("channelId") String channelId, + @Body TokenExchangeRequest exchangeRequest + ); + + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens getAadTokens" }) + @POST("api/usertoken/GetAadTokens") + CompletableFuture> getAadTokens( + @Query("userId") String userId, + @Query("connectionName") String connectionName, + @Body AadResourceUrls aadResourceUrls, + @Query("channelId") String channelId + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens signOut" }) + @HTTP(path = "api/usertoken/SignOut", method = "DELETE", hasBody = true) + CompletableFuture> signOut( + @Query("userId") String userId, + @Query("connectionName") String connectionName, + @Query("channelId") String channelId + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens signOut" }) + @HTTP(path = "api/usertoken/SignOut", method = "DELETE", hasBody = true) + CompletableFuture> signOut(@Query("userId") String userId); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens getTokenStatus" }) + @GET("api/usertoken/GetTokenStatus") + CompletableFuture> getTokenStatus( + @Query("userId") String userId, + @Query("channelId") String channelId, + @Query("include") String include + ); + + @Headers({ "Content-Type: application/json; charset=utf-8", + "x-ms-logging-context: com.microsoft.bot.schema.UserTokens sendEmulateOAuthCards" }) + @POST("api/usertoken/emulateOAuthCards") + CompletableFuture> sendEmulateOAuthCards( + @Query("emulate") boolean emulate + ); + } + + /** + * + * @param userId the String value + * @param connectionName the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the TokenResponse object + */ + @Override + public CompletableFuture getToken(String userId, String connectionName) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (connectionName == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter connectionName is required and cannot be null." + )); + } + + final String channelId = null; + final String code = null; + return service.getToken(userId, connectionName, channelId, code) + .thenApply(responseBodyResponse -> { + try { + return getTokenDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getToken", responseBodyResponse); + } + }); + } + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param channelId the String value + * @param code the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the TokenResponse object + */ + @Override + public CompletableFuture getToken( + String userId, + String connectionName, + String channelId, + String code + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (connectionName == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter connectionName is required and cannot be null." + )); + } + + return service.getToken(userId, connectionName, channelId, code) + .thenApply(responseBodyResponse -> { + try { + return getTokenDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getToken", responseBodyResponse); + } + }); + } + + private ServiceResponse getTokenDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + .newInstance(this.client.serializerAdapter()) + + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_NOT_FOUND, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param channelId the String value + * @param exchangeRequest a TokenExchangeRequest + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the TokenResponse object + */ + @Override + public CompletableFuture exchangeToken( + String userId, + String connectionName, + String channelId, + TokenExchangeRequest exchangeRequest + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (connectionName == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter connectionName is required and cannot be null." + )); + } + if (channelId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter channelId is required and cannot be null." + )); + } + if (exchangeRequest == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter exchangeRequest is required and cannot be null." + )); + } + + return service.exchangeToken(userId, connectionName, channelId, exchangeRequest) + .thenApply(responseBodyResponse -> { + try { + return exchangeTokenDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getToken", responseBodyResponse); + } + }); + } + + private ServiceResponse exchangeTokenDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + .newInstance(this.client.serializerAdapter()) + + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_NOT_FOUND, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param aadResourceUrls the AadResourceUrls value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Map<String, TokenResponse> object + */ + @Override + public CompletableFuture> getAadTokens( + String userId, + String connectionName, + AadResourceUrls aadResourceUrls + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (connectionName == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter connectionName is required and cannot be null." + )); + } + if (aadResourceUrls == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter aadResourceUrls is required and cannot be null." + )); + } + + Validator.validate(aadResourceUrls); + final String channelId = null; + return service.getAadTokens(userId, connectionName, aadResourceUrls, channelId) + .thenApply(responseBodyResponse -> { + try { + return getAadTokensDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getAadTokens", responseBodyResponse); + } + }); + } + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param aadResourceUrls the AadResourceUrls value + * @param channelId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Map<String, TokenResponse> object + */ + @Override + public CompletableFuture> getAadTokens( + String userId, + String connectionName, + AadResourceUrls aadResourceUrls, + String channelId + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (connectionName == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter connectionName is required and cannot be null." + )); + } + if (aadResourceUrls == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter aadResourceUrls is required and cannot be null." + )); + } + + return Async.tryCompletable(() -> { + Validator.validate(aadResourceUrls); + return service.getAadTokens(userId, connectionName, aadResourceUrls, channelId) + .thenApply(responseBodyResponse -> { + try { + return getAadTokensDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getAadTokens", responseBodyResponse); + } + + }); + }); + } + + private ServiceResponse> getAadTokensDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + ., ErrorResponseException>newInstance( + this.client.serializerAdapter() + ) + + .register(HttpURLConnection.HTTP_OK, new TypeToken>() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * + * @param userId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Object object + */ + @Override + public CompletableFuture signOut(String userId) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + + return service.signOut(userId).thenApply(responseBodyResponse -> { + try { + return signOutDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("signOut", responseBodyResponse); + } + }); + } + + /** + * + * @param userId the String value + * @param connectionName the String value + * @param channelId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the Object object + */ + @Override + public CompletableFuture signOut( + String userId, + String connectionName, + String channelId + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + if (connectionName == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter connectionName is required and cannot be null." + )); + } + if (channelId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter channelId is required and cannot be null." + )); + } + + return service.signOut(userId, connectionName, channelId) + .thenApply(responseBodyResponse -> { + try { + return signOutDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("signOut", responseBodyResponse); + } + }); + } + + private ServiceResponse signOutDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + .newInstance(this.client.serializerAdapter()) + + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_NO_CONTENT, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * + * @param userId the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the List<TokenStatus> object + */ + @Override + public CompletableFuture> getTokenStatus(String userId) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + + final String channelId = null; + final String include = null; + return service.getTokenStatus(userId, channelId, include) + .thenApply(responseBodyResponse -> { + try { + return getTokenStatusDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getTokenStatus", responseBodyResponse); + } + }); + } + + /** + * + * @param userId the String value + * @param channelId the String value + * @param include the String value + * @throws IllegalArgumentException thrown if parameters fail the validation + * @return the observable to the List<TokenStatus> object + */ + @Override + public CompletableFuture> getTokenStatus( + String userId, + String channelId, + String include + ) { + if (userId == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "Parameter userId is required and cannot be null." + )); + } + + return service.getTokenStatus(userId, channelId, include) + .thenApply(responseBodyResponse -> { + try { + return getTokenStatusDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("getTokenStatus", responseBodyResponse); + } + }); + } + + private ServiceResponse> getTokenStatusDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return this.client.restClient() + .responseBuilderFactory() + ., ErrorResponseException>newInstance(this.client.serializerAdapter()) + + .register(HttpURLConnection.HTTP_OK, new TypeToken>() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } + + /** + * Send a dummy OAuth card when the bot is being used on the Emulator for testing without fetching a real token. + * + * @param emulateOAuthCards Indicates whether the Emulator should emulate the OAuth card. + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture sendEmulateOAuthCards(boolean emulateOAuthCards) { + return service.sendEmulateOAuthCards(emulateOAuthCards) + .thenApply(responseBodyResponse -> { + try { + return sendEmulateOAuthCardsDelegate(responseBodyResponse).body(); + } catch (ErrorResponseException e) { + throw e; + } catch (Throwable t) { + throw new ErrorResponseException("sendEmulateOAuthCards", responseBodyResponse); + } + }); + } + + private ServiceResponse sendEmulateOAuthCardsDelegate( + Response response + ) throws ErrorResponseException, IOException, IllegalArgumentException { + + return client.restClient() + .responseBuilderFactory() + .newInstance(client.serializerAdapter()) + .register(HttpURLConnection.HTTP_OK, new TypeToken() { + }.getType()) + .register(HttpURLConnection.HTTP_ACCEPTED, new TypeToken() { + }.getType()) + .registerError(ErrorResponseException.class) + .build(response); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/package-info.java new file mode 100644 index 000000000..2f0b4b7bc --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/rest/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the implementation classes for com.microsoft.bot.connector.rest. + */ +package com.microsoft.bot.connector.rest; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/TeamsConnectorClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/TeamsConnectorClient.java new file mode 100644 index 000000000..24e15aab1 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/TeamsConnectorClient.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.teams; + +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; + +/** + * Teams operations. + */ +public interface TeamsConnectorClient extends AutoCloseable { + /** + * Gets the REST client. + * + * @return the {@link RestClient} object. + */ + RestClient getRestClient(); + + /** + * Returns the base url for this ConnectorClient. + * + * @return The base url. + */ + String baseUrl(); + + /** + * Returns the credentials in use. + * + * @return The ServiceClientCredentials in use. + */ + ServiceClientCredentials credentials(); + + /** + * Gets the User-Agent header for the client. + * + * @return the user agent string. + */ + String getUserAgent(); + + /** + * Gets the preferred language for the response.. + * + * @return the acceptLanguage value. + */ + String getAcceptLanguage(); + + /** + * Sets the preferred language for the response.. + * + * @param acceptLanguage the acceptLanguage value. + */ + void setAcceptLanguage(String acceptLanguage); + + /** + * Gets the retry timeout in seconds for Long Running Operations. Default value + * is 30.. + * + * @return the timeout value. + */ + int getLongRunningOperationRetryTimeout(); + + /** + * Sets the retry timeout in seconds for Long Running Operations. Default value + * is 30. + * + * @param timeout the longRunningOperationRetryTimeout value. + */ + void setLongRunningOperationRetryTimeout(int timeout); + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. is true. + * + * @return the generateClientRequestId value. + */ + boolean getGenerateClientRequestId(); + + /** + * When set to true a unique x-ms-client-request-id value is generated and + * included in each request. Default is true. + * + * @param generateClientRequestId the generateClientRequestId value. + */ + void setGenerateClientRequestId(boolean generateClientRequestId); + + /** + * Gets TeamsOperations. + * + * @return A TeamsOperations object. + */ + TeamsOperations getTeams(); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/TeamsOperations.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/TeamsOperations.java new file mode 100644 index 000000000..b65d1fdd0 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/TeamsOperations.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + *

+ * Code generated by Microsoft (R) AutoRest Code Generator. + * Changes may cause incorrect behavior and will be lost if the code is + * regenerated. + */ + +package com.microsoft.bot.connector.teams; + +import com.microsoft.bot.schema.teams.ConversationList; +import com.microsoft.bot.schema.teams.MeetingInfo; +import com.microsoft.bot.schema.teams.TeamDetails; + +import com.microsoft.bot.schema.teams.TeamsMeetingParticipant; +import java.util.concurrent.CompletableFuture; + +/** + * Teams operations. + */ +public interface TeamsOperations { + /** + * Fetches channel list for a given team. + * + * @param teamId The team id. + * @return A ConversationList object. + */ + CompletableFuture fetchChannelList(String teamId); + + /** + * Fetches details related to a team. + * + * @param teamId The team id. + * @return The TeamDetails + */ + CompletableFuture fetchTeamDetails(String teamId); + + /** + * Fetches Teams meeting participant details. + * @param meetingId Teams meeting id + * @param participantId Teams meeting participant id + * @param tenantId Teams meeting tenant id + * @return TeamsMeetingParticipant + */ + CompletableFuture fetchParticipant( + String meetingId, + String participantId, + String tenantId + ); + + /** + * Fetches information related to a Teams meeting. + * @param meetingId Meeting Id. + * @return The details related to a team. + */ + default CompletableFuture fetchMeetingInfo(String meetingId) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(new Exception("fetchMeetingInfo not implemented")); + return result; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/package-info.java new file mode 100644 index 000000000..a054d1a8d --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/teams/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the implementation classes for com.microsoft.bot.connector.teams. + */ +package com.microsoft.bot.connector.teams; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/Base64Url.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/Base64Url.java new file mode 100644 index 000000000..01e0f0ce2 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/Base64Url.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.google.common.io.BaseEncoding; + +import java.util.Arrays; + +/** + * Simple wrapper over Base64Url encoded byte array used during serialization/deserialization. + */ +public final class Base64Url { + + /** + * The Base64Url encoded bytes. + */ + private final byte[] bytes; + + /** + * Creates a new Base64Url object with the specified encoded string. + * + * @param string The encoded string. + */ + private Base64Url(String string) { + if (string == null) { + this.bytes = null; + } else { + this.bytes = string.getBytes(); + } + } + + /** + * Encode a byte array into Base64Url encoded bytes. + * + * @param bytes The byte array to encode. + * @return a Base64Url instance + */ + public static Base64Url encode(byte[] bytes) { + if (bytes == null) { + return new Base64Url(null); + } else { + return new Base64Url(BaseEncoding.base64Url().omitPadding().encode(bytes)); + } + } + + /** + * Returns the underlying encoded byte array. + * + * @return The underlying encoded byte array. + */ + public byte[] encodedBytes() { + return bytes; + } + + /** + * Decode the bytes and return. + * + * @return The decoded byte array. + */ + public byte[] decodedBytes() { + if (this.bytes == null) { + return null; + } + return BaseEncoding.base64Url().decode(new String(bytes)); + } + + @Override + public String toString() { + return new String(bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof Base64Url)) { + return false; + } + + Base64Url rhs = (Base64Url) obj; + return Arrays.equals(this.bytes, rhs.encodedBytes()); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/CollectionFormat.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/CollectionFormat.java new file mode 100644 index 000000000..1b8438897 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/CollectionFormat.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +@SuppressWarnings("checkstyle:linelength") +/** + * Swagger collection format to use for joining {@link java.util.List} parameters in + * paths, queries, and headers. + * See https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields-7. + */ +public enum CollectionFormat { + /** + * Comma separated values. + * E.g. foo,bar + */ + CSV(","), + /** + * Space separated values. + * E.g. foo bar + */ + SSV(" "), + /** + * Tab separated values. + * E.g. foo\tbar + */ + TSV("\t"), + /** + * Pipe(|) separated values. + * E.g. foo|bar + */ + PIPES("|"), + /** + * Corresponds to multiple parameter instances instead of multiple values + * for a single instance. + * E.g. foo=bar&foo=baz + */ + MULTI("&"); + + /** + * The delimiter separating the values. + */ + private final String delimiter; + + /** + * Creates an instance of the enum. + * @param delimiter the delimiter as a string. + */ + CollectionFormat(String delimiter) { + this.delimiter = delimiter; + } + + /** + * Gets the delimiter used to join a list of parameters. + * @return the delimiter of the current collection format. + */ + public String getDelimiter() { + return delimiter; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ExpandableStringEnum.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ExpandableStringEnum.java new file mode 100644 index 000000000..5b5f8fc19 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ExpandableStringEnum.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Base implementation for expandable, single string enums. + * @param a specific expandable enum type + */ +public abstract class ExpandableStringEnum> { + + private static ConcurrentMap> valuesByName = null; + + private String name; + private Class clazz; + + private static String uniqueKey(Class clazz, String name) { + if (clazz != null) { + return (clazz.getName() + "#" + name).toLowerCase(); + } else { + throw new IllegalArgumentException(); + } + } + + @SuppressWarnings("unchecked") + protected T withNameValue(String name, T value, Class clazz) { + if (valuesByName == null) { + valuesByName = new ConcurrentHashMap(); + } + this.name = name; + this.clazz = clazz; + ((ConcurrentMap) valuesByName).put(uniqueKey(clazz, name), value); + return (T) this; + } + + @SuppressWarnings("unchecked") + protected static > T fromString(String name, Class clazz) { + if (name == null) { + return null; + } else if (valuesByName != null) { + T value = (T) valuesByName.get(uniqueKey(clazz, name)); + if (value != null) { + return value; + } + } + + try { + T value = clazz.newInstance(); + return value.withNameValue(name, value, clazz); + } catch (InstantiationException e) { + return null; + } catch (IllegalAccessException e) { + return null; + } + } + + @SuppressWarnings("unchecked") + protected static > Collection values(Class clazz) { + // Make a copy of all values + Collection> values = new ArrayList<>( + valuesByName.values()); + + Collection list = new HashSet<>(); + for (ExpandableStringEnum value : values) { + if (value.getClass().isAssignableFrom(clazz)) { + list.add((T) value); + } + } + + return list; + } + + @Override + @JsonValue + public String toString() { + return this.name; + } + + @Override + public int hashCode() { + return uniqueKey(this.clazz, this.name).hashCode(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object obj) { + if (!clazz.isAssignableFrom(obj.getClass())) { + return false; + } else if (obj == this) { + return true; + } else if (this.name == null) { + return ((ExpandableStringEnum) obj).name == null; + } else { + return this.name.equals(((ExpandableStringEnum) obj).name); + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/LogLevel.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/LogLevel.java new file mode 100644 index 000000000..be2971b7f --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/LogLevel.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +/** + * Describes the level of HTTP traffic to log. + */ +public enum LogLevel { + /** + * Logging is turned off. + */ + NONE, + + /** + * Logs only URLs, HTTP methods, and time to finish the request. + */ + BASIC, + + /** + * Logs everything in BASIC, plus all the request and response headers. + */ + HEADERS, + + /** + * Logs everything in BASIC, plus all the request and response body. + * Note that only payloads in plain text or plan text encoded in GZIP + * will be logged. + */ + BODY, + + /** + * Logs everything in HEADERS and BODY. + */ + BODY_AND_HEADERS; + + private boolean prettyJson = false; + + /** + * @return if the JSON payloads will be prettified when log level is set + * to BODY or BODY_AND_HEADERS. Default is false. + */ + public boolean isPrettyJson() { + return prettyJson; + } + + /** + * Specifies whether to log prettified JSON. + * @param prettyJson true if JSON paylods are prettified. + * @return the enum object + */ + public LogLevel withPrettyJson(boolean prettyJson) { + this.prettyJson = prettyJson; + return this; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/RestClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/RestClient.java new file mode 100644 index 000000000..f44e26026 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/RestClient.java @@ -0,0 +1,540 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.microsoft.azure.management.apigeneration.Beta; +import com.microsoft.azure.management.apigeneration.Beta.SinceVersion; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.interceptors.BaseUrlHandler; +import com.microsoft.bot.restclient.interceptors.CustomHeadersInterceptor; +import com.microsoft.bot.restclient.interceptors.LoggingInterceptor; +import com.microsoft.bot.restclient.interceptors.RequestIdHeaderInterceptor; +import com.microsoft.bot.restclient.interceptors.UserAgentInterceptor; +import com.microsoft.bot.restclient.protocol.Environment; +import com.microsoft.bot.restclient.protocol.ResponseBuilder; +import com.microsoft.bot.restclient.protocol.SerializerAdapter; +import com.microsoft.bot.restclient.retry.RetryHandler; +import com.microsoft.bot.restclient.retry.RetryStrategy; +import okhttp3.Authenticator; +import okhttp3.ConnectionPool; +import okhttp3.Dispatcher; +import okhttp3.Interceptor; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import okio.AsyncTimeout; +import retrofit2.Retrofit; + +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.Proxy; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * An instance of this class stores the client information for making REST calls. + */ +public final class RestClient { + /** The {@link okhttp3.OkHttpClient} object. */ + private final OkHttpClient httpClient; + /** The {@link retrofit2.Retrofit} object. */ + private final Retrofit retrofit; + /** The original builder for this rest client. */ + private final RestClient.Builder builder; + + private RestClient(OkHttpClient httpClient, + Retrofit retrofit, + RestClient.Builder builder) { + this.httpClient = httpClient; + this.retrofit = retrofit; + this.builder = builder; + } + + /** + * @return the headers interceptor. + */ + public CustomHeadersInterceptor headers() { + return builder.customHeadersInterceptor; + } + + /** + * @return the current serializer adapter. + */ + public SerializerAdapter serializerAdapter() { + return builder.serializerAdapter; + } + + /** + * @return the current respnose builder factory. + */ + public ResponseBuilder.Factory responseBuilderFactory() { + return builder.responseBuilderFactory; + } + + /** + * @return the {@link OkHttpClient} instance + */ + public OkHttpClient httpClient() { + return httpClient; + } + + /** + * @return the {@link Retrofit} instance + */ + public Retrofit retrofit() { + return retrofit; + } + + /** + * @return the credentials attached to this REST client + */ + public ServiceClientCredentials credentials() { + return builder.credentials; + } + + /** + * @return the current HTTP traffic logging level + */ + public LogLevel logLevel() { + return builder.loggingInterceptor.logLevel(); + } + + /** + * Set the current HTTP traffic logging level. + * @param logLevel the logging level enum + * @return the RestClient itself + */ + public RestClient withLogLevel(LogLevel logLevel) { + builder.loggingInterceptor.withLogLevel(logLevel); + return this; + } + + /** + * Create a new builder for a new Rest Client with the same configurations on this one. + * @return a RestClient builder + */ + public RestClient.Builder newBuilder() { + return new Builder(this); + } + + /** + * Closes the HTTP client and recycles the resources associated. The threads will + * be recycled after 60 seconds of inactivity. + */ + @Beta(SinceVersion.V1_1_0) + public void close() { + httpClient.dispatcher().executorService().shutdown(); + httpClient.connectionPool().evictAll(); + synchronized (httpClient.connectionPool()) { + httpClient.connectionPool().notifyAll(); + } + synchronized (AsyncTimeout.class) { + AsyncTimeout.class.notifyAll(); + } + } + + /** + * Closes the HTTP client, recycles the resources associated, and waits + * for 60 seconds for all the threads to be recycled. + * + * @throws InterruptedException thrown when the 60-sec wait is interrupted + */ + @Beta(SinceVersion.V1_1_0) + public void closeAndWait() throws InterruptedException { + close(); + Thread.sleep(60000); + } + + /** + * The builder class for building a REST client. + */ + public static class Builder { + /** The dynamic base URL with variables wrapped in "{" and "}". */ + private String baseUrl; + /** The builder to build an {@link OkHttpClient}. */ + private OkHttpClient.Builder httpClientBuilder; + /** The builder to build a {@link Retrofit}. */ + private final Retrofit.Builder retrofitBuilder; + /** The credentials to authenticate. */ + private ServiceClientCredentials credentials; + /** The credentials interceptor. */ + private Interceptor credentialsInterceptor; + /** The interceptor to handle custom headers. */ + private CustomHeadersInterceptor customHeadersInterceptor; + /** The value for 'User-Agent' header. */ + private String userAgent; + /** The adapter for serializations and deserializations. */ + private SerializerAdapter serializerAdapter; + /** The builder factory for response builders. */ + private ResponseBuilder.Factory responseBuilderFactory; + /** The logging interceptor to use. */ + private LoggingInterceptor loggingInterceptor; + /** The strategy used for retry failed requests. */ + private RetryStrategy retryStrategy; + /** The dispatcher for OkHttp to handle requests. */ + private Dispatcher dispatcher; + /** If set to true, the dispatcher thread pool rather than RxJava schedulers will be used to schedule requests. */ + private boolean useHttpClientThreadPool; + /** The connection pool in use for OkHttp. */ + private ConnectionPool connectionPool; + + /** + * Creates an instance of the builder with a base URL to the service. + */ + public Builder() { + this(new OkHttpClient.Builder(), new Retrofit.Builder()); + } + + private Builder(final RestClient restClient) { + this(restClient.httpClient.newBuilder(), new Retrofit.Builder()); + this.httpClientBuilder.readTimeout(restClient.httpClient.readTimeoutMillis(), TimeUnit.MILLISECONDS); + this.httpClientBuilder.connectTimeout(restClient.httpClient.connectTimeoutMillis(), TimeUnit.MILLISECONDS); + this.httpClientBuilder.interceptors().clear(); + this.httpClientBuilder.networkInterceptors().clear(); + this.baseUrl = restClient.retrofit.baseUrl().toString(); + this.responseBuilderFactory = restClient.builder.responseBuilderFactory; + this.serializerAdapter = restClient.builder.serializerAdapter; + this.useHttpClientThreadPool = restClient.builder.useHttpClientThreadPool; + if (restClient.builder.credentials != null) { + this.credentials = restClient.builder.credentials; + } + if (restClient.retrofit.callbackExecutor() != null) { + this.withCallbackExecutor(restClient.retrofit.callbackExecutor()); + } + for (Interceptor interceptor : restClient.httpClient.interceptors()) { + if (interceptor instanceof UserAgentInterceptor) { + this.userAgent = ((UserAgentInterceptor) interceptor).userAgent(); + } else if (interceptor instanceof RetryHandler) { + this.retryStrategy = ((RetryHandler) interceptor).strategy(); + } else if (interceptor instanceof CustomHeadersInterceptor) { + this.customHeadersInterceptor = new CustomHeadersInterceptor(); + this.customHeadersInterceptor.addHeaderMultimap(((CustomHeadersInterceptor) interceptor).headers()); + } else if (interceptor != restClient.builder.credentialsInterceptor) { + this.withInterceptor(interceptor); + } + } + for (Interceptor interceptor : restClient.httpClient.networkInterceptors()) { + if (interceptor instanceof LoggingInterceptor) { + LoggingInterceptor old = (LoggingInterceptor) interceptor; + this.loggingInterceptor = new LoggingInterceptor(old.logLevel()); + } else { + this.withNetworkInterceptor(interceptor); + } + } + } + + /** + * Creates an instance of the builder with a base URL and 2 custom builders. + * + * @param httpClientBuilder the builder to build an {@link OkHttpClient}. + * @param retrofitBuilder the builder to build a {@link Retrofit}. + */ + public Builder(OkHttpClient.Builder httpClientBuilder, Retrofit.Builder retrofitBuilder) { + if (httpClientBuilder == null) { + throw new IllegalArgumentException("httpClientBuilder == null"); + } + if (retrofitBuilder == null) { + throw new IllegalArgumentException("retrofitBuilder == null"); + } + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + customHeadersInterceptor = new CustomHeadersInterceptor(); + // Set up OkHttp client + this.httpClientBuilder = httpClientBuilder + .cookieJar(new JavaNetCookieJar(cookieManager)) + .readTimeout(120, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .addInterceptor(new RequestIdHeaderInterceptor()) + .addInterceptor(new BaseUrlHandler()); + this.retrofitBuilder = retrofitBuilder; + this.loggingInterceptor = new LoggingInterceptor(LogLevel.NONE); + this.useHttpClientThreadPool = false; + } + + /** + * Sets the dynamic base URL. + * + * @param baseUrl the base URL to use. + * @return the builder itself for chaining. + */ + public Builder withBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Sets the base URL with the default from the Environment. + * + * @param environment the environment to use + * @param endpoint the environment endpoint the application is accessing + * @return the builder itself for chaining + */ + public Builder withBaseUrl(Environment environment, Environment.Endpoint endpoint) { + this.baseUrl = environment.url(endpoint); + return this; + } + + /** + * Sets the serialization adapter. + * + * @param serializerAdapter the adapter to a serializer + * @return the builder itself for chaining + */ + public Builder withSerializerAdapter(SerializerAdapter serializerAdapter) { + this.serializerAdapter = serializerAdapter; + return this; + } + + /** + * Sets the response builder factory. + * + * @param responseBuilderFactory the response builder factory + * @return the builder itself for chaining + */ + public Builder withResponseBuilderFactory(ResponseBuilder.Factory responseBuilderFactory) { + this.responseBuilderFactory = responseBuilderFactory; + return this; + } + + /** + * Sets the credentials. + * + * @param credentials the credentials object. + * @return the builder itself for chaining. + */ + public Builder withCredentials(ServiceClientCredentials credentials) { + if (credentials == null) { + throw new NullPointerException("credentials == null"); + } + this.credentials = credentials; + return this; + } + + /** + * Sets the user agent header. + * + * @param userAgent the user agent header. + * @return the builder itself for chaining. + */ + public Builder withUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + /** + * Sets the HTTP log level. + * + * @param logLevel the {@link LogLevel} enum. + * @return the builder itself for chaining. + */ + public Builder withLogLevel(LogLevel logLevel) { + if (logLevel == null) { + throw new NullPointerException("logLevel == null"); + } + this.loggingInterceptor.withLogLevel(logLevel); + return this; + } + + /** + * Add an interceptor the Http client pipeline. + * + * @param interceptor the interceptor to add. + * @return the builder itself for chaining. + */ + public Builder withInterceptor(Interceptor interceptor) { + if (interceptor == null) { + throw new NullPointerException("interceptor == null"); + } + httpClientBuilder.addInterceptor(interceptor); + return this; + } + + /** + * Add an interceptor the network layer of Http client pipeline. + * + * @param networkInterceptor the interceptor to add. + * @return the builder itself for chaining. + */ + public Builder withNetworkInterceptor(Interceptor networkInterceptor) { + if (networkInterceptor == null) { + throw new NullPointerException("networkInterceptor == null"); + } + httpClientBuilder.addNetworkInterceptor(networkInterceptor); + return this; + } + + /** + * Set the read timeout on the HTTP client. Default is 10 seconds. + * + * @param timeout the timeout numeric value + * @param unit the time unit for the numeric value + * @return the builder itself for chaining + */ + public Builder withReadTimeout(long timeout, TimeUnit unit) { + httpClientBuilder.readTimeout(timeout, unit); + return this; + } + + /** + * Set the connection timeout on the HTTP client. Default is 10 seconds. + * + * @param timeout the timeout numeric value + * @param unit the time unit for the numeric value + * @return the builder itself for chaining + */ + public Builder withConnectionTimeout(long timeout, TimeUnit unit) { + httpClientBuilder.connectTimeout(timeout, unit); + return this; + } + + /** + * Set the maximum idle connections for the HTTP client. Default is 5. + * + * @param maxIdleConnections the maximum idle connections + * @return the builder itself for chaining + * @deprecated use {@link #withConnectionPool(ConnectionPool)} instead + */ + @Deprecated + public Builder withMaxIdleConnections(int maxIdleConnections) { + this.connectionPool = new ConnectionPool(maxIdleConnections, 5, TimeUnit.MINUTES); + return this; + } + + /** + * Sets the connection pool for the Http client. + * @param connectionPool the OkHttp 3 connection pool to use + * @return the builder itself for chaining + */ + public Builder withConnectionPool(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + return this; + } + + /** + * Sets whether to use the thread pool in OkHttp client or RxJava schedulers. + * If set to true, the thread pool in OkHttp client will be used. Default is false. + * @param useHttpClientThreadPool whether to use the thread pool in Okhttp client. Default is false. + * @return the builder itself for chaining + */ + public Builder useHttpClientThreadPool(boolean useHttpClientThreadPool) { + this.useHttpClientThreadPool = useHttpClientThreadPool; + return this; + } + + /** + * Sets the dispatcher used in OkHttp client. This is also where to set + * the thread pool for executing HTTP requests. + * @param dispatcher the dispatcher to use + * @return the builder itself for chaining + */ + public Builder withDispatcher(Dispatcher dispatcher) { + this.dispatcher = dispatcher; + return this; + } + + /** + * Sets the executor for async callbacks to run on. + * + * @param executor the executor to execute the callbacks. + * @return the builder itself for chaining + */ + public Builder withCallbackExecutor(Executor executor) { + retrofitBuilder.callbackExecutor(executor); + return this; + } + + /** + * Sets the proxy for the HTTP client. + * + * @param proxy the proxy to use + * @return the builder itself for chaining + */ + public Builder withProxy(Proxy proxy) { + httpClientBuilder.proxy(proxy); + return this; + } + + /** + * Sets the proxy authenticator for the HTTP client. + * + * @param proxyAuthenticator the proxy authenticator to use + * @return the builder itself for chaining + */ + public Builder withProxyAuthenticator(Authenticator proxyAuthenticator) { + httpClientBuilder.proxyAuthenticator(proxyAuthenticator); + return this; + } + + /** + * Adds a retry strategy to the client. + * @param strategy the retry strategy to add + * @return the builder itself for chaining + */ + public Builder withRetryStrategy(RetryStrategy strategy) { + this.retryStrategy = strategy; + return this; + } + + /** + * Build a RestClient with all the current configurations. + * + * @return a {@link RestClient}. + */ + public RestClient build() { + UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor(); + if (userAgent != null) { + userAgentInterceptor.withUserAgent(userAgent); + } + if (baseUrl == null) { + throw new IllegalArgumentException("Please set base URL."); + } + if (!baseUrl.endsWith("/")) { + baseUrl += "/"; + } + if (responseBuilderFactory == null) { + throw new IllegalArgumentException("Please set response builder factory."); + } + if (serializerAdapter == null) { + throw new IllegalArgumentException("Please set serializer adapter."); + } + + if (this.credentials != null) { + int interceptorCount = httpClientBuilder.interceptors().size(); + this.credentials.applyCredentialsFilter(httpClientBuilder); + // store the interceptor + if (httpClientBuilder.interceptors().size() > interceptorCount) { + credentialsInterceptor = httpClientBuilder.interceptors().get(interceptorCount); + } + } + + RetryHandler retryHandler; + if (retryStrategy == null) { + retryHandler = new RetryHandler(); + } else { + retryHandler = new RetryHandler(retryStrategy); + } + + if (connectionPool != null) { + httpClientBuilder = httpClientBuilder.connectionPool(connectionPool); + } + if (dispatcher != null) { + httpClientBuilder = httpClientBuilder.dispatcher(dispatcher); + } + + OkHttpClient httpClient = httpClientBuilder + .addInterceptor(userAgentInterceptor) + .addInterceptor(customHeadersInterceptor) + .addInterceptor(retryHandler) + .addNetworkInterceptor(loggingInterceptor) + .build(); + + return new RestClient(httpClient, + retrofitBuilder + .baseUrl(baseUrl) + .client(httpClient) + .addConverterFactory(serializerAdapter.converterFactory()) + .build(), + this); + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/RestException.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/RestException.java new file mode 100644 index 000000000..936c97e8e --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/RestException.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * Exception thrown for an invalid response with custom error information. + */ +public class RestException extends RuntimeException { + /** + * Information about the associated HTTP response. + */ + private final Response response; + + /** + * The HTTP response body. + */ + private Object body; + + /** + * Initializes a new instance of the RestException class. + * + * @param message the exception message or the response content if a message is not available + * @param response the HTTP response + */ + public RestException(String message, Response response) { + super(message); + this.response = response; + } + + /** + * Initializes a new instance of the RestException class. + * + * @param message the exception message or the response content if a message is not available + * @param response the HTTP response + * @param body the deserialized response body + */ + public RestException(String message, Response response, Object body) { + super(message); + this.response = response; + this.body = body; + } + + /** + * @return information about the associated HTTP response + */ + public Response response() { + return response; + } + + /** + * @return the HTTP response body + */ + public Object body() { + return body; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceCallback.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceCallback.java new file mode 100644 index 000000000..3418769ff --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceCallback.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +/** + * The callback used for client side asynchronous operations. + * + * @param the type of the response + */ +public interface ServiceCallback { + /** + * Override this method to handle REST call failures. + * + * @param t the exception thrown from the pipeline. + */ + void failure(Throwable t); + + /** + * Override this method to handle successful REST call results. + * + * @param result the result object. + */ + void success(T result); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceClient.java new file mode 100644 index 000000000..e182c4320 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceClient.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.google.common.hash.Hashing; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.protocol.SerializerAdapter; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import java.net.NetworkInterface; +import java.util.Enumeration; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * ServiceClient is the abstraction for accessing REST operations and their payload data types. + */ +public abstract class ServiceClient { + /** + * The RestClient instance storing all information needed for making REST calls. + */ + private final RestClient restClient; + + /** + * Initializes a new instance of the ServiceClient class. + * + * @param baseUrl the service endpoint + */ + protected ServiceClient(String baseUrl) { + this(baseUrl, new OkHttpClient.Builder(), new Retrofit.Builder()); + } + + /** + * Initializes a new instance of the ServiceClient class. + * + * @param baseUrl the service base uri + * @param clientBuilder the http client builder + * @param restBuilder the retrofit rest client builder + */ + protected ServiceClient(String baseUrl, OkHttpClient.Builder clientBuilder, Retrofit.Builder restBuilder) { + this(new RestClient.Builder(clientBuilder, restBuilder) + .withBaseUrl(baseUrl) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()) + .withSerializerAdapter(new JacksonAdapter()) + .build()); + } + + protected ServiceClient(String baseUrl, ServiceClientCredentials credentials) { + this(baseUrl, credentials, new OkHttpClient.Builder(), new Retrofit.Builder()); + } + + /** + * Initializes a new instance of the ServiceClient class. + * + * @param baseUrl the service base uri + * @param credentials the credentials + * @param clientBuilder the http client builder + * @param restBuilder the retrofit rest client builder + */ + protected ServiceClient(String baseUrl, ServiceClientCredentials credentials, OkHttpClient.Builder clientBuilder, Retrofit.Builder restBuilder) { + this(new RestClient.Builder(clientBuilder, restBuilder) + .withBaseUrl(baseUrl) + .withCredentials(credentials) + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()) + .build()); + } + + /** + * Initializes a new instance of the ServiceClient class. + * + * @param restClient the REST client + */ + protected ServiceClient(RestClient restClient) { + this.restClient = restClient; + } + + /** + * @return the {@link RestClient} instance. + */ + public RestClient restClient() { + return restClient; + } + + /** + * @return the Retrofit instance. + */ + public Retrofit retrofit() { + return restClient.retrofit(); + } + + /** + * @return the HTTP client. + */ + public OkHttpClient httpClient() { + return this.restClient.httpClient(); + } + + /** + * @return the adapter to a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. + */ + public SerializerAdapter serializerAdapter() { + return this.restClient.serializerAdapter(); + } + + /** + * The default User-Agent header. Override this method to override the user agent. + * + * @return the user agent string. + */ + public String userAgent() { + return String.format("Azure-SDK-For-Java/%s OS:%s MacAddressHash:%s Java:%s", + getClass().getPackage().getImplementationVersion(), + OS, + MAC_ADDRESS_HASH, + JAVA_VERSION); + } + + private static final String MAC_ADDRESS_HASH; + private static final String OS; + private static final String JAVA_VERSION; + + static { + OS = System.getProperty("os.name") + "/" + System.getProperty("os.version"); + String macAddress = "Unknown"; + + try { + Enumeration networks = NetworkInterface.getNetworkInterfaces(); + while (networks.hasMoreElements()) { + NetworkInterface network = networks.nextElement(); + byte[] mac = network.getHardwareAddress(); + + if (mac != null) { + macAddress = Hashing.sha256().hashBytes(mac).toString(); + break; + } + } + } catch (Throwable ignore) { //NOPMD + // It's okay ignore mac address hash telemetry + } + MAC_ADDRESS_HASH = macAddress; + String version = System.getProperty("java.version"); + JAVA_VERSION = version != null ? version : "Unknown"; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponse.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponse.java new file mode 100644 index 000000000..60791aeec --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponse.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * An instance of this class holds a response object and a raw REST response. + * + * @param the type of the response + */ +public class ServiceResponse { + /** + * The response body object. + */ + private T body; + + /** + * The retrofit response wrapper containing information about the REST response. + */ + private Response response; + + /** + * The retrofit response wrapper if it's returned from a HEAD operation. + */ + private Response headResponse; + + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param body deserialized response object + * @param response raw REST response + */ + public ServiceResponse(T body, Response response) { + this.body = body; + this.response = response; + } + + /** + * Instantiate a ServiceResponse instance with a response from a HEAD operation. + * + * @param headResponse raw REST response from a HEAD operation + */ + public ServiceResponse(Response headResponse) { + this.headResponse = headResponse; + } + + /** + * Gets the response object. + * @return the response object. Null if there isn't one. + */ + public T body() { + return this.body; + } + + /** + * Sets the response object. + * + * @param body the response object. + * @return the ServiceResponse object itself + */ + public ServiceResponse withBody(T body) { + this.body = body; + return this; + } + + /** + * Gets the raw REST response. + * + * @return the raw REST response. + */ + public Response response() { + return response; + } + + /** + * Gets the raw REST response from a HEAD operation. + * + * @return the raw REST response from a HEAD operation. + */ + public Response headResponse() { + return headResponse; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java new file mode 100644 index 000000000..c94f7bbd6 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.Maps; +import com.microsoft.bot.restclient.protocol.ResponseBuilder; +import com.microsoft.bot.restclient.protocol.SerializerAdapter; +import okhttp3.ResponseBody; +import retrofit2.Response; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +/** + * The builder for building a {@link ServiceResponse}. + * + * @param The return type the caller expects from the REST response. + * @param the exception to throw in case of error. + */ +public final class ServiceResponseBuilder implements ResponseBuilder { + /** + * A mapping of HTTP status codes and their corresponding return types. + */ + private final Map responseTypes; + + /** + * The exception type to thrown in case of error. + */ + private Class exceptionType; + + /** + * The mapperAdapter used for deserializing the response. + */ + private final SerializerAdapter serializerAdapter; + + private boolean throwOnGet404; + + /** + * Create a ServiceResponseBuilder instance. + * + * @param serializerAdapter the serialization utils to use for deserialization operations + */ + private ServiceResponseBuilder(SerializerAdapter serializerAdapter) { + this.serializerAdapter = serializerAdapter; + this.responseTypes = new HashMap<>(); + this.exceptionType = RestException.class; + this.responseTypes.put(0, Object.class); + this.throwOnGet404 = false; + } + + @Override + public ServiceResponseBuilder register(int statusCode, final Type type) { + this.responseTypes.put(statusCode, type); + return this; + } + + + @Override + public ServiceResponseBuilder registerError(final Class type) { + this.exceptionType = type; + try { + Method f = type.getDeclaredMethod("body"); + this.responseTypes.put(0, f.getReturnType()); + } catch (NoSuchMethodException e) { + // AutoRestException always has a body. Register Object as a fallback plan. + this.responseTypes.put(0, Object.class); + } + return this; + } + + /** + * Register all the mappings from a response status code to a response + * destination type stored in a {@link Map}. + * + * @param responseTypes the mapping from response status codes to response types. + * @return the same builder instance. + */ + public ServiceResponseBuilder registerAll(Map responseTypes) { + this.responseTypes.putAll(responseTypes); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public ServiceResponse build(Response response) throws IOException { + if (response == null) { + return null; + } + + int statusCode = response.code(); + ResponseBody responseBody; + if (response.isSuccessful()) { + responseBody = response.body(); + } else { + responseBody = response.errorBody(); + } + + if (responseTypes.containsKey(statusCode)) { + return new ServiceResponse<>((T) buildBody(statusCode, responseBody), response); + } else if (response.isSuccessful() && responseTypes.size() == 1) { + return new ServiceResponse<>((T) buildBody(statusCode, responseBody), response); + } else if (!throwOnGet404 && "GET".equals(response.raw().request().method()) && statusCode == 404) { + return new ServiceResponse<>(null, response); + } else { + try { + String responseContent = ""; + if (responseBody != null) { + responseContent = responseBody.source().buffer().clone().readUtf8(); + } + throw exceptionType.getConstructor(String.class, Response.class, (Class) responseTypes.get(0)) + .newInstance("Status code " + statusCode + ", " + responseContent, response, buildBody(statusCode, responseBody)); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IOException("Status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() + + " cannot be created.", e); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public ServiceResponse buildEmpty(Response response) throws IOException { + int statusCode = response.code(); + if (responseTypes.containsKey(statusCode)) { + return new ServiceResponse<>(response); + } else if (response.isSuccessful() && responseTypes.size() == 1) { + return new ServiceResponse<>(response); + } else { + try { + throw exceptionType.getConstructor(String.class, Response.class) + .newInstance("Status code " + statusCode, response); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IOException("Status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() + + " cannot be created.", e); + } + } + } + + @Override + public ServiceResponseWithHeaders buildWithHeaders(final Response response, Class headerType) throws IOException { + ServiceResponse bodyResponse = build(response); + THeader headers = serializerAdapter.deserialize( + serializerAdapter.serialize(Maps.asMap(response.headers().names(), + s -> response.headers().get(s) + )), + headerType); + return new ServiceResponseWithHeaders<>(bodyResponse.body(), headers, bodyResponse.response()); + } + + @Override + public ServiceResponseWithHeaders buildEmptyWithHeaders(final Response response, Class headerType) throws IOException { + ServiceResponse bodyResponse = buildEmpty(response); + THeader headers = serializerAdapter.deserialize( + serializerAdapter.serialize(Maps.asMap(response.headers().names(), + s -> response.headers().get(s) + )), + headerType); + ServiceResponseWithHeaders serviceResponse = new ServiceResponseWithHeaders<>(headers, bodyResponse.headResponse()); + serviceResponse.withBody(bodyResponse.body()); + return serviceResponse; + } + + /** + * Builds the body object from the HTTP status code and returned response + * body undeserialized and wrapped in {@link ResponseBody}. + * + * @param statusCode the HTTP status code + * @param responseBody the response body + * @return the response body, deserialized + * @throws IOException thrown for any deserialization errors + */ + private Object buildBody(int statusCode, ResponseBody responseBody) throws IOException { + if (responseBody == null) { + return null; + } + + Type type; + if (responseTypes.containsKey(statusCode)) { + type = responseTypes.get(statusCode); + } else if (responseTypes.get(0) != Object.class) { + type = responseTypes.get(0); + } else { + type = new TypeReference() { }.getType(); + } + + // Void response + if (type == Void.class) { + return null; + } + // Return raw response if InputStream is the target type + else if (type == InputStream.class) { + return responseBody.byteStream(); + } + // Deserialize + else { + String responseContent = responseBody.source().buffer().readUtf8(); + if (responseContent.length() <= 0) { + return null; + } + return serializerAdapter.deserialize(responseContent, type); + } + } + + /** + * @return the exception type to thrown in case of error. + */ + public Class exceptionType() { + return exceptionType; + } + + /** + * Check if the returned status code will be considered a success for + * this builder. + * + * @param statusCode the status code to check + * @return true if it's a success, false otherwise. + */ + public boolean isSuccessful(int statusCode) { + return responseTypes != null && responseTypes.containsKey(statusCode); + } + + /** + * Specifies whether to throw on 404 responses from a GET call. + * @param throwOnGet404 true if to throw; false to simply return null. Default is false. + * @return the response builder itself + */ + public ServiceResponseBuilder withThrowOnGet404(boolean throwOnGet404) { + this.throwOnGet404 = throwOnGet404; + return this; + } + + /** + * A factory to create a service response builder. + */ + public static final class Factory implements ResponseBuilder.Factory { + @Override + public ServiceResponseBuilder newInstance(final SerializerAdapter serializerAdapter) { + return new ServiceResponseBuilder<>(serializerAdapter); + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseWithHeaders.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseWithHeaders.java new file mode 100644 index 000000000..58e69f7e0 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseWithHeaders.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import okhttp3.ResponseBody; +import retrofit2.Response; + +/** + * An instance of this class holds a response object and a raw REST response. + * + * @param the type of the response + * @param the type of the response header object + */ +public final class ServiceResponseWithHeaders extends ServiceResponse { + /** + * The response headers object. + */ + private final THeader headers; + + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param body deserialized response object + * @param headers deserialized response header object + * @param response raw REST response + */ + public ServiceResponseWithHeaders(TBody body, THeader headers, Response response) { + super(body, response); + this.headers = headers; + } + + /** + * Instantiate a ServiceResponse instance with a response object and a raw REST response. + * + * @param headers deserialized response header object + * @param response raw REST response + */ + public ServiceResponseWithHeaders(THeader headers, Response response) { + super(response); + this.headers = headers; + } + + /** + * Gets the response headers. + * @return the response headers. Null if there isn't one. + */ + public THeader headers() { + return this.headers; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/SkipParentValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/SkipParentValidation.java new file mode 100644 index 000000000..9eb40ae53 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/SkipParentValidation.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used for notifying the validator to skip + * validation for the properties in the parent class. + * + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SkipParentValidation { +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/Validator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/Validator.java new file mode 100644 index 000000000..ef95f8653 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/Validator.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +/** + * Validates user provided parameters are not null if they are required. + */ +public final class Validator { + /** + * Hidden constructor for utility class. + */ + private Validator() { } + + /** + * Validates a user provided required parameter to be not null. + * An {@link IllegalArgumentException} is thrown if a property fails the validation. + * + * @param parameter the parameter to validate + * @throws IllegalArgumentException thrown when the Validator determines the argument is invalid + */ + public static void validate(Object parameter) { + // Validation of top level payload is done outside + if (parameter == null) { + return; + } + + Class parameterType = parameter.getClass(); + TypeToken parameterToken = TypeToken.of(parameterType); + if (Primitives.isWrapperType(parameterType)) { + parameterToken = parameterToken.unwrap(); + } + if (parameterToken.isPrimitive() + || parameterType.isEnum() + || parameterType == Class.class + || parameterToken.isSupertypeOf(OffsetDateTime.class) + || parameterToken.isSupertypeOf(ZonedDateTime.class) + || parameterToken.isSupertypeOf(String.class) + || parameterToken.isSupertypeOf(Period.class)) { + return; + } + + Annotation skipParentAnnotation = parameterType.getAnnotation(SkipParentValidation.class); + + if (skipParentAnnotation == null) { + for (Class c : parameterToken.getTypes().classes().rawTypes()) { + validateClass(c, parameter); + } + } else { + validateClass(parameterType, parameter); + } + } + + private static void validateClass(Class c, Object parameter) { + // Ignore checks for Object type. + if (c.isAssignableFrom(Object.class)) { + return; + } + for (Field field : c.getDeclaredFields()) { + field.setAccessible(true); + int mod = field.getModifiers(); + // Skip static fields since we don't have any, skip final fields since users can't modify them + if (Modifier.isFinal(mod) || Modifier.isStatic(mod)) { + continue; + } + JsonProperty annotation = field.getAnnotation(JsonProperty.class); + // Skip read-only properties (WRITE_ONLY) + if (annotation != null && annotation.access().equals(JsonProperty.Access.WRITE_ONLY)) { + continue; + } + Object property; + try { + property = field.get(parameter); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + if (property == null) { + if (annotation != null && annotation.required()) { + throw new IllegalArgumentException(field.getName() + " is required and cannot be null."); + } + } else { + try { + Class propertyType = property.getClass(); + if (TypeToken.of(List.class).isSupertypeOf(propertyType)) { + List items = (List) property; + for (Object item : items) { + Validator.validate(item); + } + } + else if (TypeToken.of(Map.class).isSupertypeOf(propertyType)) { + Map entries = (Map) property; + for (Map.Entry entry : entries.entrySet()) { + Validator.validate(entry.getKey()); + Validator.validate(entry.getValue()); + } + } + else if (parameter.getClass() != propertyType) { + Validator.validate(property); + } + } catch (IllegalArgumentException ex) { + if (ex.getCause() == null) { + // Build property chain + throw new IllegalArgumentException(field.getName() + "." + ex.getMessage()); + } else { + throw ex; + } + } + } + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/BasicAuthenticationCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/BasicAuthenticationCredentials.java new file mode 100644 index 000000000..14ffbfa50 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/BasicAuthenticationCredentials.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.credentials; + +import okhttp3.OkHttpClient; + +/** + * Basic Auth credentials for use with a REST Service Client. + */ +public class BasicAuthenticationCredentials implements ServiceClientCredentials { + + /** + * Basic auth UserName. + */ + private final String userName; + + /** + * Basic auth password. + */ + private final String password; + + /** + * Instantiates a new basic authentication credential. + * + * @param withUserName basic auth user name + * @param withPassword basic auth password + */ + public BasicAuthenticationCredentials(String withUserName, String withPassword) { + this.userName = withUserName; + this.password = withPassword; + } + + /** + * @return the user name of the credential + */ + public String getUserName() { + return userName; + } + + /** + * @return the password of the credential + */ + protected String getPassword() { + return password; + } + + /** + * Apply the credentials to the HTTP client builder. + * + * @param clientBuilder the builder for building up an {@link OkHttpClient} + */ + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new BasicAuthenticationCredentialsInterceptor(this)); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/BasicAuthenticationCredentialsInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/BasicAuthenticationCredentialsInterceptor.java new file mode 100644 index 000000000..d0069ea0d --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/BasicAuthenticationCredentialsInterceptor.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.credentials; + +import com.google.common.io.BaseEncoding; +import java.nio.charset.StandardCharsets; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * Basic Auth credentials interceptor for placing a basic auth credential into request headers. + */ +final class BasicAuthenticationCredentialsInterceptor implements Interceptor { + /** + * The credentials instance to apply to the HTTP client pipeline. + */ + private final BasicAuthenticationCredentials credentials; + + /** + * Initialize a BasicAuthenticationCredentialsFilter class with a + * BasicAuthenticationCredentials credential. + * + * @param withCredentials a BasicAuthenticationCredentials instance + */ + BasicAuthenticationCredentialsInterceptor(BasicAuthenticationCredentials withCredentials) { + this.credentials = withCredentials; + } + + /** + * Handle OKHttp intercept. + * @param chain okhttp3 Chain + * @return okhttp3 Response + * @throws IOException IOException during http IO. + */ + @Override + public Response intercept(Chain chain) throws IOException { + String auth = credentials.getUserName() + ":" + credentials.getPassword(); + auth = BaseEncoding.base64().encode(auth.getBytes(StandardCharsets.UTF_8)); + Request newRequest = chain.request().newBuilder() + .header("Authorization", "Basic " + auth) + .build(); + return chain.proceed(newRequest); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/ServiceClientCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/ServiceClientCredentials.java new file mode 100644 index 000000000..d98bdb991 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/ServiceClientCredentials.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.credentials; + +import okhttp3.OkHttpClient; + +/** + * ServiceClientCredentials is the abstraction for credentials used by + * ServiceClients accessing REST services. + */ +public interface ServiceClientCredentials { + /** + * Apply the credentials to the HTTP client builder. + * + * @param clientBuilder the builder for building up an {@link OkHttpClient} + */ + void applyCredentialsFilter(OkHttpClient.Builder clientBuilder); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/TokenCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/TokenCredentials.java new file mode 100644 index 000000000..1774b8fb2 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/TokenCredentials.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.credentials; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +/** + * Token based credentials for use with a REST Service Client. + */ +public class TokenCredentials implements ServiceClientCredentials { + /** The authentication scheme. */ + private final String scheme; + + /** The secure token. */ + private final String token; + + /** + * Initializes a new instance of the TokenCredentials. + * + * @param withScheme scheme to use. If null, defaults to Bearer + * @param withToken valid token + */ + public TokenCredentials(String withScheme, String withToken) { + if (withScheme == null) { + withScheme = "Bearer"; + } + this.scheme = withScheme; + this.token = withToken; + } + + /** + * Get the secure token. Override this method to provide a mechanism + * for acquiring tokens. + * + * @param request the context of the HTTP request + * @return the secure token. + */ + protected String getToken(Request request) { + return token; + } + + /** + * Get the authentication scheme. + * + * @return the authentication scheme + */ + protected String getScheme() { + return scheme; + } + + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new TokenCredentialsInterceptor(this)); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/TokenCredentialsInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/TokenCredentialsInterceptor.java new file mode 100644 index 000000000..2ce4abb49 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/TokenCredentialsInterceptor.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.credentials; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * Token credentials filter for placing a token credential into request headers. + */ +final class TokenCredentialsInterceptor implements Interceptor { + /** + * The credentials instance to apply to the HTTP client pipeline. + */ + private final TokenCredentials credentials; + + /** + * Initialize a TokenCredentialsFilter class with a + * TokenCredentials credential. + * + * @param credentials a TokenCredentials instance + */ + TokenCredentialsInterceptor(TokenCredentials credentials) { + this.credentials = credentials; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request newRequest = chain.request().newBuilder() + .header("Authorization", credentials.getScheme() + " " + credentials.getToken(chain.request())) + .build(); + return chain.proceed(newRequest); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/package-info.java new file mode 100644 index 000000000..17ac009b9 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/credentials/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * The package provides 2 basic credential classes that work with AutoRest + * generated clients for authentication purposes. + */ +package com.microsoft.bot.restclient.credentials; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/BaseUrlHandler.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/BaseUrlHandler.java new file mode 100644 index 000000000..099f70f18 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/BaseUrlHandler.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.interceptors; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * Handles dynamic replacements on base URL. The arguments must be in pairs + * with the string in raw URL to replace as replacements[i] and the dynamic + * part as replacements[i+1]. E.g. {subdomain}.microsoft.com can be set + * dynamically by setting header x-ms-parameterized-host: "{subdomain}, azure" + */ +public final class BaseUrlHandler implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String parameters = request.header("x-ms-parameterized-host"); + if (parameters != null && !parameters.isEmpty()) { + String[] replacements = parameters.split(", "); + if (replacements.length % 2 != 0) { + throw new IllegalArgumentException("Must provide a replacement value for each pattern"); + } + String baseUrl = request.url().toString(); + for (int i = 0; i < replacements.length; i += 2) { + baseUrl = baseUrl.replaceAll("(?i)\\Q" + replacements[i] + "\\E", replacements[i + 1]); + } + baseUrl = removeRedundantProtocol(baseUrl); + HttpUrl baseHttpUrl = HttpUrl.parse(baseUrl); + request = request.newBuilder() + .url(baseHttpUrl) + .removeHeader("x-ms-parameterized-host") + .build(); + } + return chain.proceed(request); + } + + private String removeRedundantProtocol(String url) { + int last = url.lastIndexOf("://") - 1; + while (last >= 0 && Character.isLetter(url.charAt(last))) { + --last; + } + return url.substring(last + 1); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/CustomHeadersInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/CustomHeadersInterceptor.java new file mode 100644 index 000000000..2eb4c6cb8 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/CustomHeadersInterceptor.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.interceptors; + +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An instance of this class enables adding custom headers in client requests + * when added to the {@link okhttp3.OkHttpClient} interceptors. + */ +public final class CustomHeadersInterceptor implements Interceptor { + /** + * @return the currently stored custom headers + */ + public Map> headers() { + return headers; + } + + /** + * A mapping of custom headers. + */ + private final Map> headers; + + /** + * Initialize an instance of {@link CustomHeadersInterceptor} class. + */ + public CustomHeadersInterceptor() { + headers = new HashMap<>(); + } + + /** + * Initialize an instance of {@link CustomHeadersInterceptor} class. + * + * @param key the key for the header + * @param value the value of the header + */ + public CustomHeadersInterceptor(String key, String value) { + this(); + addHeader(key, value); + } + + /** + * Add a single header key-value pair. If one with the name already exists, + * it gets replaced. + * + * @param name the name of the header. + * @param value the value of the header. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor replaceHeader(String name, String value) { + this.headers.put(name, new ArrayList<>()); + this.headers.get(name).add(value); + return this; + } + + /** + * Add a single header key-value pair. If one with the name already exists, + * both stay in the header map. + * + * @param name the name of the header. + * @param value the value of the header. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeader(String name, String value) { + if (!this.headers.containsKey(name)) { + this.headers.put(name, new ArrayList<>()); + } + this.headers.get(name).add(value); + return this; + } + + /** + * Add all headers in a {@link Headers} object. + * + * @param headers an OkHttp {@link Headers} object. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeaders(Headers headers) { + this.headers.putAll(headers.toMultimap()); + return this; + } + + /** + * Add all headers in a header map. + * + * @param headers a map of headers. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeaderMap(Map headers) { + for (Map.Entry header : headers.entrySet()) { + this.headers.put(header.getKey(), Collections.singletonList(header.getValue())); + } + return this; + } + + /** + * Add all headers in a header multimap. + * + * @param headers a multimap of headers. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor addHeaderMultimap(Map> headers) { + this.headers.putAll(headers); + return this; + } + + /** + * Remove a header. + * + * @param name the name of the header to remove. + * @return the interceptor instance itself. + */ + public CustomHeadersInterceptor removeHeader(String name) { + this.headers.remove(name); + return this; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder(); + for (Map.Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + builder = builder.header(header.getKey(), value); + } + } + return chain.proceed(builder.build()); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/LoggingInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/LoggingInterceptor.java new file mode 100644 index 000000000..684254f80 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/LoggingInterceptor.java @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.interceptors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.io.CharStreams; +import com.microsoft.bot.restclient.LogLevel; +import java.nio.charset.StandardCharsets; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +/** + * An OkHttp interceptor that handles logging of HTTP requests and responses. + */ +public class LoggingInterceptor implements Interceptor { + private static final String LOGGING_HEADER = "x-ms-logging-context"; + private static final String BODY_LOGGING = "x-ms-body-logging"; + private static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); + private LogLevel logLevel; + + /** + * Creates an interceptor with a LogLevel enum. + * @param logLevel the level of traffic to log + */ + public LoggingInterceptor(LogLevel logLevel) { + this.logLevel = logLevel; + } + + /** + * Process the log using an SLF4j logger and an HTTP message. + * @param logger the SLF4j logger with the context of the request + * @param s the message for logging + */ + protected void log(Logger logger, String s) { + logger.info(s); + } + + @SuppressWarnings("checkstyle:DesignForExtension") + @Override + public Response intercept(Chain chain) throws IOException { + // get logger + Request request = chain.request(); + String context = request.header(LOGGING_HEADER); + String bodyLoggingHeader = request.header(BODY_LOGGING); + boolean bodyLogging = bodyLoggingHeader == null || Boolean.parseBoolean(bodyLoggingHeader); + if (context == null) { + context = ""; + } + Logger logger = LoggerFactory.getLogger(context); + + // log URL + if (logLevel != LogLevel.NONE) { + log(logger, String.format("--> %s %s", request.method(), request.url())); + } + // log headers + if (logLevel == LogLevel.HEADERS || logLevel == LogLevel.BODY_AND_HEADERS) { + for (String header : request.headers().names()) { + if (!LOGGING_HEADER.equals(header)) { + log(logger, String.format("%s: %s", header, Joiner.on(", ").join(request.headers(header)))); + } + } + } + // log body + if (bodyLogging + && (logLevel == LogLevel.BODY || logLevel == LogLevel.BODY_AND_HEADERS) + && request.body() != null) { + + Buffer buffer = new Buffer(); + request.body().writeTo(buffer); + + Charset charset = StandardCharsets.UTF_8; + MediaType contentType = request.body().contentType(); + if (contentType != null) { + charset = contentType.charset(charset); + } + + if (isPlaintext(buffer)) { + String content = buffer.clone().readString(charset); + if (logLevel.isPrettyJson()) { + try { + content = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( + MAPPER.readValue(content, JsonNode.class)); + } catch (Exception ignore) { //NOPMD + // swallow, keep original content + } + } + log(logger, String.format("%s-byte body:\n%s", request.body().contentLength(), content)); + log(logger, "--> END " + request.method()); + } else { + log(logger, "--> END " + request.method() + " (binary " + + request.body().contentLength() + "-byte body omitted)"); + } + } + + long startNs = System.nanoTime(); + Response response; + try { + response = chain.proceed(request); + } catch (Exception e) { + if (logLevel != LogLevel.NONE) { + log(logger, "<-- HTTP FAILED: " + e); + } + throw e; + } + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + + ResponseBody responseBody = response.body(); + long contentLength = responseBody.contentLength(); + String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length"; + + // log URL + if (logLevel != LogLevel.NONE) { + log(logger, String.format("<-- %s %s %s (%s ms, %s body)", + response.code(), response.message(), response.request().url(), tookMs, bodySize)); + } + + // log headers + if (logLevel == LogLevel.HEADERS || logLevel == LogLevel.BODY_AND_HEADERS) { + for (String header : response.headers().names()) { + log(logger, String.format("%s: %s", header, Joiner.on(", ").join(response.headers(header)))); + } + } + + // log body + if (bodyLogging + && (logLevel == LogLevel.BODY || logLevel == LogLevel.BODY_AND_HEADERS) + && response.body() != null) { + + BufferedSource source = responseBody.source(); + source.request(Long.MAX_VALUE); // Buffer the entire body. + Buffer buffer = source.buffer(); + + Charset charset = StandardCharsets.UTF_8; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + try { + charset = contentType.charset(charset); + } catch (UnsupportedCharsetException e) { + log(logger, "Couldn't decode the response body; charset is likely malformed."); + log(logger, "<-- END HTTP"); + return response; + } + } + + boolean gzipped = response.header("content-encoding") != null + && StringUtils.containsIgnoreCase(response.header("content-encoding"), "gzip"); + + if (!isPlaintext(buffer) && !gzipped) { + log(logger, "<-- END HTTP (binary " + buffer.size() + "-byte body omitted)"); + return response; + } + + String content; + if (gzipped) { + content = CharStreams.toString( + new InputStreamReader(new GZIPInputStream(buffer.clone().inputStream()))); + } else { + content = buffer.clone().readString(charset); + } + if (logLevel.isPrettyJson()) { + try { + content = MAPPER.writerWithDefaultPrettyPrinter() + .writeValueAsString(MAPPER.readValue(content, JsonNode.class)); + } catch (Exception ignore) { //NOPMD + // swallow, keep original content + } + } + log(logger, String.format("%s-byte body:\n%s", buffer.size(), content)); + log(logger, "<-- END HTTP"); + } + return response; + } + + /** + * @return the current logging level. + */ + public LogLevel logLevel() { + return logLevel; + } + + /** + * Sets the current logging level. + * @param logLevel the new logging level + * @return the interceptor + */ + public LoggingInterceptor withLogLevel(LogLevel logLevel) { + this.logLevel = logLevel; + return this; + } + + private static boolean isPlaintext(Buffer buffer) { + try { + Buffer prefix = new Buffer(); + long byteCount = buffer.size() < 64 ? buffer.size() : 64; + buffer.copyTo(prefix, 0, byteCount); + for (int i = 0; i < 16; i++) { + if (prefix.exhausted()) { + break; + } + int codePoint = prefix.readUtf8CodePoint(); + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false; + } + } + return true; + } catch (EOFException e) { + return false; + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/RequestIdHeaderInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/RequestIdHeaderInterceptor.java new file mode 100644 index 000000000..7c0549039 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/RequestIdHeaderInterceptor.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.interceptors; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.UUID; + +/** + * An instance of this class puts an UUID in the request header. Azure uses + * the request id as the unique identifier for + */ +public final class RequestIdHeaderInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + if (request.header("x-ms-client-request-id") == null) { + request = chain.request().newBuilder() + .header("x-ms-client-request-id", UUID.randomUUID().toString()) + .build(); + } + return chain.proceed(request); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/UserAgentInterceptor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/UserAgentInterceptor.java new file mode 100644 index 000000000..6dcc7a2dc --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/UserAgentInterceptor.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.interceptors; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * User agent interceptor for putting a 'User-Agent' header in the request. + */ +public final class UserAgentInterceptor implements Interceptor { + /** + * The default user agent header. + */ + private static final String DEFAULT_USER_AGENT_HEADER = "AutoRest-Java"; + + /** + * The user agent header string. + */ + private String userAgent; + + /** + * Initialize an instance of {@link UserAgentInterceptor} class with the default + * 'User-Agent' header. + */ + public UserAgentInterceptor() { + this.userAgent = DEFAULT_USER_AGENT_HEADER; + } + + /** + * Overwrite the User-Agent header. + * + * @param userAgent the new user agent value. + * @return the user agent interceptor itself + */ + public UserAgentInterceptor withUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + /** + * Append a text to the User-Agent header. + * + * @param userAgent the user agent value to append. + * @return the user agent interceptor itself + */ + public UserAgentInterceptor appendUserAgent(String userAgent) { + this.userAgent += " " + userAgent; + return this; + } + + /** + * @return the current user agent string. + */ + public String userAgent() { + return userAgent; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String header = request.header("User-Agent"); + if (header == null) { + header = DEFAULT_USER_AGENT_HEADER; + } + if (!DEFAULT_USER_AGENT_HEADER.equals(userAgent)) { + if (header.equals(DEFAULT_USER_AGENT_HEADER)) { + header = userAgent; + } else { + header = userAgent + " " + header; + } + } + request = chain.request().newBuilder() + .header("User-Agent", header) + .build(); + return chain.proceed(request); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/package-info.java new file mode 100644 index 000000000..d0191e6d8 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/interceptors/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * The package contains default interceptors for making HTTP requests. + */ +package com.microsoft.bot.restclient.interceptors; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/package-info.java new file mode 100644 index 000000000..cf2ef3499 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * The package contains the runtime classes required for AutoRest generated + * clients to compile and function. To learn more about AutoRest generator, + * see https://github.com/azure/autorest. + */ +package com.microsoft.bot.restclient; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/Environment.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/Environment.java new file mode 100644 index 000000000..6e8f64c21 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/Environment.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.protocol; + +/** + * An collection of endpoints in a region or a cloud. + */ +public interface Environment { + /** + * An endpoint identifier used for the provider to get a URL. + */ + interface Endpoint { + /** + * @return a unique identifier for the endpoint in the environment + */ + String identifier(); + } + + /** + * Provides a URL for the endpoint. + * @param endpoint the endpoint the client is accessing + * @return the URL to make HTTP requests to + */ + String url(Endpoint endpoint); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/ResponseBuilder.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/ResponseBuilder.java new file mode 100644 index 000000000..9492f6f81 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/ResponseBuilder.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.protocol; + +import com.microsoft.bot.restclient.RestException; +import com.microsoft.bot.restclient.ServiceResponse; +import com.microsoft.bot.restclient.ServiceResponseWithHeaders; +import okhttp3.ResponseBody; +import retrofit2.Response; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * Defines an interface that can process a Retrofit 2 response + * into a deserialized body or an exception, depending on the + * status code registered. + * + * @param the body type if the status code is considered successful + * @param the exception type if the status code is considered a failure + */ +public interface ResponseBuilder { + /** + * Register a mapping from a response status code to a response destination type. + * + * @param statusCode the status code. + * @param type the type to deserialize. + * @return the same builder instance. + */ + ResponseBuilder register(int statusCode, final Type type); + + /** + * Register a destination type for errors with models. + * + * @param type the type to deserialize. + * @return the same builder instance. + */ + ResponseBuilder registerError(final Class type); + + /** + * Build a ServiceResponse instance from a REST call response and a + * possible error. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid and deserialized into the specified destination + * type. If the status code is not registered, the response will be + * considered invalid and deserialized into the specified error type if + * there is one. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @return a ServiceResponse instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + ServiceResponse build(Response response) throws IOException; + + /** + * Build a ServiceResponse instance from a REST call response and a + * possible error, which does not have a response body. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid. If the status code is not registered, the + * response will be considered invalid. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @return a ServiceResponse instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + ServiceResponse buildEmpty(Response response) throws IOException; + + /** + * Build a ServiceResponseWithHeaders instance from a REST call response, a header + * in JSON format, and a possible error. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid and deserialized into the specified destination + * type. If the status code is not registered, the response will be + * considered invalid and deserialized into the specified error type if + * there is one. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @param headerType the type of the header + * @param the type of the header + * @return a ServiceResponseWithHeaders instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + ServiceResponseWithHeaders buildWithHeaders(Response response, Class headerType) throws IOException; + + /** + * Build a ServiceResponseWithHeaders instance from a REST call response, a header + * in JSON format, and a possible error, which does not have a response body. + * + *

+ * If the status code in the response is registered, the response will + * be considered valid. If the status code is not registered, the + * response will be considered invalid. An AutoRestException is also thrown. + *

+ * + * @param response the {@link Response} instance from REST call + * @param headerType the type of the header + * @param the type of the header + * @return a ServiceResponseWithHeaders instance of generic type {@link T} + * @throws E exceptions from the REST call + * @throws IOException exceptions from deserialization + */ + ServiceResponseWithHeaders buildEmptyWithHeaders(Response response, Class headerType) throws IOException; + + /** + * A factory that creates a builder based on the return type and the exception type. + */ + interface Factory { + /** + * Returns a response builder instance. This can be created new or cached. + * + * @param the type of the return object + * @param the type of the exception + * @param serializerAdapter the serializer adapter to deserialize the response + * @return a response builder instance + */ + ResponseBuilder newInstance(final SerializerAdapter serializerAdapter); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/SerializerAdapter.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/SerializerAdapter.java new file mode 100644 index 000000000..dc28358d6 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/SerializerAdapter.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.protocol; + +import com.microsoft.bot.restclient.CollectionFormat; +import retrofit2.Converter; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; + +/** + * This interface defines the behaviors an adapter of a serializer + * needs to implement. + * + * @param the original serializer + */ +public interface SerializerAdapter { + /** + * @return the adapted original serializer + */ + T serializer(); + + /** + * @return a converter factory for Retrofit + */ + Converter.Factory converterFactory(); + + /** + * Serializes an object into a JSON string. + * + * @param object the object to serialize. + * @return the serialized string. Null if the object to serialize is null. + * @throws IOException exception from serialization. + */ + String serialize(Object object) throws IOException; + + /** + * Serializes an object into a raw string. The leading and trailing quotes will be trimmed. + * + * @param object the object to serialize. + * @return the serialized string. Null if the object to serialize is null. + */ + String serializeRaw(Object object); + + /** + * Serializes a list into a string with the delimiter specified with the + * Swagger collection format joining each individual serialized items in + * the list. + * + * @param list the list to serialize. + * @param format the Swagger collection format. + * @return the serialized string + */ + String serializeList(List list, CollectionFormat format); + + /** + * Deserializes a string into a {@link U} object using the current {@link T}. + * + * @param value the string value to deserialize. + * @param the type of the deserialized object. + * @param type the type to deserialize. + * @return the deserialized object. + * @throws IOException exception in deserialization + */ + U deserialize(String value, final Type type) throws IOException; +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/package-info.java new file mode 100644 index 000000000..da523673b --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/protocol/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * The package contains classes that interfaces defining the behaviors + * of the necessary components of a Rest Client. + */ +package com.microsoft.bot.restclient.protocol; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/ExponentialBackoffRetryStrategy.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/ExponentialBackoffRetryStrategy.java new file mode 100644 index 000000000..f04bf6b18 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/ExponentialBackoffRetryStrategy.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.retry; + +import okhttp3.Response; + +/** + * A retry strategy with backoff parameters for calculating the exponential delay between retries. + */ +public final class ExponentialBackoffRetryStrategy extends RetryStrategy { + /** + * Represents the default amount of time used when calculating a random delta in the exponential + * delay between retries. + */ + public static final int DEFAULT_CLIENT_BACKOFF = 1000 * 10; + /** + * Represents the default maximum amount of time used when calculating the exponential + * delay between retries. + */ + public static final int DEFAULT_MAX_BACKOFF = 1000 * 30; + /** + *Represents the default minimum amount of time used when calculating the exponential + * delay between retries. + */ + public static final int DEFAULT_MIN_BACKOFF = 1000; + + /** + * The value that will be used to calculate a random delta in the exponential delay + * between retries. + */ + private final int deltaBackoff; //NOPMD + /** + * The maximum backoff time. + */ + private final int maxBackoff; //NOPMD + /** + * The minimum backoff time. + */ + private final int minBackoff; //NOPMD + /** + * The maximum number of retry attempts. + */ + private final int retryCount; + + /** + * Initializes a new instance of the {@link ExponentialBackoffRetryStrategy} class. + */ + public ExponentialBackoffRetryStrategy() { + this(DEFAULT_CLIENT_RETRY_COUNT, DEFAULT_MIN_BACKOFF, DEFAULT_MAX_BACKOFF, DEFAULT_CLIENT_BACKOFF); + } + + /** + * Initializes a new instance of the {@link ExponentialBackoffRetryStrategy} class. + * + * @param retryCount The maximum number of retry attempts. + * @param minBackoff The minimum backoff time. + * @param maxBackoff The maximum backoff time. + * @param deltaBackoff The value that will be used to calculate a random delta in the exponential delay + * between retries. + */ + @SuppressWarnings("checkstyle:HiddenField") + public ExponentialBackoffRetryStrategy(int retryCount, int minBackoff, int maxBackoff, int deltaBackoff) { + this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DEFAULT_FIRST_FAST_RETRY); + } + + /** + * Initializes a new instance of the {@link ExponentialBackoffRetryStrategy} class. + * + * @param name The name of the retry strategy. + * @param retryCount The maximum number of retry attempts. + * @param minBackoff The minimum backoff time. + * @param maxBackoff The maximum backoff time. + * @param deltaBackoff The value that will be used to calculate a random delta in the exponential delay + * between retries. + * @param firstFastRetry true to immediately retry in the first attempt; otherwise, false. The subsequent + * retries will remain subject to the configured retry interval. + */ + @SuppressWarnings("checkstyle:HiddenField") + public ExponentialBackoffRetryStrategy(String name, int retryCount, int minBackoff, int maxBackoff, + int deltaBackoff, boolean firstFastRetry) { + super(name, firstFastRetry); + this.retryCount = retryCount; + this.minBackoff = minBackoff; + this.maxBackoff = maxBackoff; + this.deltaBackoff = deltaBackoff; + } + + /** + * Returns if a request should be retried based on the retry count, current response, + * and the current strategy. + * + * @param retryCount The current retry attempt count. + * @param response The exception that caused the retry conditions to occur. + * @return true if the request should be retried; false otherwise. + */ + @SuppressWarnings({"checkstyle:MagicNumber", "checkstyle:HiddenField"}) + @Override + public boolean shouldRetry(int retryCount, Response response) { + int code = response.code(); + return retryCount < this.retryCount + && (code == 408 || code >= 500 && code != 501 && code != 505); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/RetryHandler.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/RetryHandler.java new file mode 100644 index 000000000..feeda4d58 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/RetryHandler.java @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.retry; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +/** + * An instance of this interceptor placed in the request pipeline handles retriable errors. + */ +public final class RetryHandler implements Interceptor { + /** + * Represents the default number of retries. + */ + private static final int DEFAULT_NUMBER_OF_ATTEMPTS = 3; + /** + * Represents the default value that will be used to calculate a random + * delta in the exponential delay between retries. + */ + private static final int DEFAULT_BACKOFF_DELTA = 1000 * 10; + /** + * Represents the default maximum backoff time. + */ + private static final int DEFAULT_MAX_BACKOFF = 1000 * 10; + /** + * Represents the default minimum backoff time. + */ + private static final int DEFAULT_MIN_BACKOFF = 1000; + + /** + * The retry strategy to use. + */ + private final RetryStrategy retryStrategy; + + /** + * @return the strategy used by this handler + */ + public RetryStrategy strategy() { + return retryStrategy; + } + + /** + * Initialized an instance of {@link RetryHandler} class. + * Sets default retry strategy base on Exponential Backoff. + */ + public RetryHandler() { + this.retryStrategy = new ExponentialBackoffRetryStrategy( + DEFAULT_NUMBER_OF_ATTEMPTS, + DEFAULT_MIN_BACKOFF, + DEFAULT_MAX_BACKOFF, + DEFAULT_BACKOFF_DELTA); + } + + /** + * Initialized an instance of {@link RetryHandler} class. + * + * @param retryStrategy retry strategy to use. + */ + public RetryHandler(RetryStrategy retryStrategy) { + this.retryStrategy = retryStrategy; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + // try the request + Response response = chain.proceed(request); + + int tryCount = 0; + while (retryStrategy.shouldRetry(tryCount, response)) { + tryCount++; + if (response.body() != null) { + response.body().close(); + } + // retry the request + response = chain.proceed(request); + } + + // otherwise just pass the original response on + return response; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/RetryStrategy.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/RetryStrategy.java new file mode 100644 index 000000000..5a8d3aaa6 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/RetryStrategy.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.retry; + +import okhttp3.Response; + +/** + * Represents a retry strategy that determines the number of retry attempts and the interval + * between retries. + */ +public abstract class RetryStrategy { + /** + * Represents the default number of retry attempts. + */ + public static final int DEFAULT_CLIENT_RETRY_COUNT = 10; + + /** + * Represents the default interval between retries. + */ + public static final int DEFAULT_RETRY_INTERVAL = 1000; + /** + * Represents the default flag indicating whether the first retry attempt will be made immediately, + * whereas subsequent retries will remain subject to the retry interval. + */ + public static final boolean DEFAULT_FIRST_FAST_RETRY = true; + + /** + * The name of the retry strategy. + */ + private final String name; + + /** + * The value indicating whether the first retry attempt will be made immediately, + * whereas subsequent retries will remain subject to the retry interval. + */ + private final boolean fastFirstRetry; + + /** + * Initializes a new instance of the {@link RetryStrategy} class. + * + * @param name The name of the retry strategy. + * @param firstFastRetry true to immediately retry in the first attempt; otherwise, false. + */ + protected RetryStrategy(String name, boolean firstFastRetry) { + this.name = name; + this.fastFirstRetry = firstFastRetry; + } + + /** + * Returns if a request should be retried based on the retry count, current response, + * and the current strategy. + * + * @param retryCount The current retry attempt count. + * @param response The exception that caused the retry conditions to occur. + * @return true if the request should be retried; false otherwise. + */ + public abstract boolean shouldRetry(int retryCount, Response response); + + /** + * Gets the name of the retry strategy. + * + * @return the name of the retry strategy. + */ + public String name() { + return name; + } + + /** + * Gets whether the first retry attempt will be made immediately. + * + * @return true if the first retry attempt will be made immediately. + * false otherwise. + */ + public boolean isFastFirstRetry() { + return fastFirstRetry; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/package-info.java new file mode 100644 index 000000000..50d38d144 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/retry/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * The package contains classes that define the retry behaviors when an error + * occurs during a REST call. + */ +package com.microsoft.bot.restclient.retry; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/AdditionalPropertiesDeserializer.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/AdditionalPropertiesDeserializer.java new file mode 100644 index 000000000..5e8319cef --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/AdditionalPropertiesDeserializer.java @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.reflect.TypeToken; +import com.microsoft.azure.management.apigeneration.Beta; +import com.microsoft.azure.management.apigeneration.Beta.SinceVersion; + +import java.io.IOException; +import java.lang.reflect.Field; + +/** + * Custom serializer for deserializing complex types with additional properties. + * If a complex type has a property named "additionalProperties" with serialized + * name empty ("") of type Map<String, Object>, all extra properties on the + * payload will be stored in this map. + */ +@Beta(SinceVersion.V1_7_0) +public final class AdditionalPropertiesDeserializer extends StdDeserializer implements ResolvableDeserializer { + /** + * The default mapperAdapter for the current type. + */ + private final JsonDeserializer defaultDeserializer; + + /** + * The object mapper for default deserializations. + */ + private final ObjectMapper mapper; + + /** + * Creates an instance of FlatteningDeserializer. + * @param vc handled type + * @param defaultDeserializer the default JSON mapperAdapter + * @param mapper the object mapper for default deserializations + */ + protected AdditionalPropertiesDeserializer(Class vc, JsonDeserializer defaultDeserializer, ObjectMapper mapper) { + super(vc); + this.defaultDeserializer = defaultDeserializer; + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default deserializations + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(final ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.setDeserializerModifier(new BeanDeserializerModifier() { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) { + for (Class c : TypeToken.of(beanDesc.getBeanClass()).getTypes().classes().rawTypes()) { + Field[] fields = c.getDeclaredFields(); + for (Field field : fields) { + if ("additionalProperties".equalsIgnoreCase(field.getName())) { + JsonProperty property = field.getAnnotation(JsonProperty.class); + if (property != null && property.value().isEmpty()) { + return new AdditionalPropertiesDeserializer(beanDesc.getBeanClass(), deserializer, mapper); + } + } + } + } + return deserializer; + } + }); + return module; + } + + @SuppressWarnings("unchecked") + @Override + public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectNode root = mapper.readTree(jp); + ObjectNode copy = root.deepCopy(); + + // compare top level fields and keep only missing fields + final Class tClass = this.defaultDeserializer.handledType(); + for (Class c : TypeToken.of(tClass).getTypes().classes().rawTypes()) { + Field[] fields = c.getDeclaredFields(); + for (Field field : fields) { + JsonProperty property = field.getAnnotation(JsonProperty.class); + if (property != null) { + String key = property.value().split("((? implements ResolvableSerializer { + /** + * The default mapperAdapter for the current type. + */ + private final JsonSerializer defaultSerializer; + + /** + * The object mapper for default serializations. + */ + private final ObjectMapper mapper; + + /** + * Creates an instance of FlatteningSerializer. + * @param vc handled type + * @param defaultSerializer the default JSON serializer + * @param mapper the object mapper for default serializations + */ + protected AdditionalPropertiesSerializer(Class vc, JsonSerializer defaultSerializer, ObjectMapper mapper) { + super(vc, false); + this.defaultSerializer = defaultSerializer; + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default serializations + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(final ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.setSerializerModifier(new BeanSerializerModifier() { + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer) { + for (Class c : TypeToken.of(beanDesc.getBeanClass()).getTypes().classes().rawTypes()) { + if (c.isAssignableFrom(Object.class)) { + continue; + } + Field[] fields = c.getDeclaredFields(); + for (Field field : fields) { + if ("additionalProperties".equalsIgnoreCase(field.getName())) { + JsonProperty property = field.getAnnotation(JsonProperty.class); + if (property != null && property.value().isEmpty()) { + return new AdditionalPropertiesSerializer(beanDesc.getBeanClass(), serializer, mapper); + } + } + } + } + return serializer; + } + }); + return module; + } + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + // serialize the original object into JsonNode + ObjectNode root = mapper.valueToTree(value); + // take additional properties node out + Entry additionalPropertiesField = null; + Iterator> fields = root.fields(); + while (fields.hasNext()) { + Entry field = fields.next(); + if ("additionalProperties".equalsIgnoreCase(field.getKey())) { + additionalPropertiesField = field; + break; + } + } + if (additionalPropertiesField != null) { + root.remove(additionalPropertiesField.getKey()); + // put each item back in + ObjectNode extraProperties = (ObjectNode) additionalPropertiesField.getValue(); + fields = extraProperties.fields(); + while (fields.hasNext()) { + Entry field = fields.next(); + root.set(field.getKey(), field.getValue()); + } + } + + jgen.writeTree(root); + } + + @Override + public void resolve(SerializerProvider provider) throws JsonMappingException { + ((ResolvableSerializer) defaultSerializer).resolve(provider); + } + + @Override + public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSerializer) throws IOException { + serialize(value, gen, provider); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/Base64UrlSerializer.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/Base64UrlSerializer.java new file mode 100644 index 000000000..8a3f39657 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/Base64UrlSerializer.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.microsoft.bot.restclient.Base64Url; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link Byte[]} objects into Base64 strings. + */ +public final class Base64UrlSerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Base64Url.class, new Base64UrlSerializer()); + return module; + } + + @Override + public void serialize(Base64Url value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(value.toString()); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/ByteArraySerializer.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/ByteArraySerializer.java new file mode 100644 index 000000000..37efe77e2 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/ByteArraySerializer.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; + +/** + * Custom serializer for serializing {@link Byte[]} objects into Base64 strings. + */ +public final class ByteArraySerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Byte[].class, new ByteArraySerializer()); + return module; + } + + @Override + public void serialize(Byte[] value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + byte[] bytes = new byte[value.length]; + for (int i = 0; i < value.length; i++) { + bytes[i] = value[i]; + } + jgen.writeBinary(bytes); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/FlatteningDeserializer.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/FlatteningDeserializer.java new file mode 100644 index 000000000..26206dac0 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/FlatteningDeserializer.java @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.reflect.TypeToken; + +import java.io.IOException; +import java.lang.reflect.Field; + +/** + * Custom serializer for deserializing complex types with wrapped properties. + * For example, a property with annotation @JsonProperty(value = "properties.name") + * will be mapped to a top level "name" property in the POJO model. + */ +public final class FlatteningDeserializer extends StdDeserializer implements ResolvableDeserializer { + /** + * The default mapperAdapter for the current type. + */ + private final JsonDeserializer defaultDeserializer; + + /** + * The object mapper for default deserializations. + */ + private final ObjectMapper mapper; + + /** + * Creates an instance of FlatteningDeserializer. + * @param vc handled type + * @param defaultDeserializer the default JSON mapperAdapter + * @param mapper the object mapper for default deserializations + */ + protected FlatteningDeserializer(Class vc, JsonDeserializer defaultDeserializer, ObjectMapper mapper) { + super(vc); + this.defaultDeserializer = defaultDeserializer; + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default deserializations + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(final ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.setDeserializerModifier(new BeanDeserializerModifier() { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) { + if (BeanDeserializer.class.isAssignableFrom(deserializer.getClass())) { + // Apply flattening deserializer on all POJO types. + return new FlatteningDeserializer(beanDesc.getBeanClass(), deserializer, mapper); + } else { + return deserializer; + } + } + }); + return module; + } + + @SuppressWarnings("unchecked") + @Override + public Object deserializeWithType(JsonParser jp, DeserializationContext cxt, TypeDeserializer tDeserializer) throws IOException { + // This method will be called by Jackson for each "Json object with TypeId" in the input wire stream + // it is trying to deserialize. + // The below variable 'currentJsonNode' will hold the JsonNode corresponds to current + // Json object this method is called to handle. + // + JsonNode currentJsonNode = mapper.readTree(jp); + final Class tClass = this.defaultDeserializer.handledType(); + for (Class c : TypeToken.of(tClass).getTypes().classes().rawTypes()) { + if (!c.isAssignableFrom(Object.class)) { + final JsonTypeInfo typeInfo = c.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null) { + String typeId = typeInfo.property(); + if (containsDot(typeId)) { + final String typeIdOnWire = unescapeEscapedDots(typeId); + JsonNode typeIdValue = ((ObjectNode) currentJsonNode).remove(typeIdOnWire); + if (typeIdValue != null) { + ((ObjectNode) currentJsonNode).set(typeId, typeIdValue); + } + } + } + } + } + return tDeserializer.deserializeTypedFromAny(newJsonParserForNode(currentJsonNode), cxt); + } + + @Override + public Object deserialize(JsonParser jp, DeserializationContext cxt) throws IOException { + // This method will be called by Jackson for each "Json object" in the input wire stream + // it is trying to deserialize. + // The below variable 'currentJsonNode' will hold the JsonNode corresponds to current + // Json object this method is called to handle. + // + JsonNode currentJsonNode = mapper.readTree(jp); + if (currentJsonNode.isNull()) { + currentJsonNode = mapper.getNodeFactory().objectNode(); + } + final Class tClass = this.defaultDeserializer.handledType(); + for (Class c : TypeToken.of(tClass).getTypes().classes().rawTypes()) { + if (!c.isAssignableFrom(Object.class)) { + for (Field classField : c.getDeclaredFields()) { + handleFlatteningForField(classField, currentJsonNode); + } + } + } + return this.defaultDeserializer.deserialize(newJsonParserForNode(currentJsonNode), cxt); + } + + @Override + public void resolve(DeserializationContext cxt) throws JsonMappingException { + ((ResolvableDeserializer) this.defaultDeserializer).resolve(cxt); + } + + /** + * Given a field of a POJO class and JsonNode corresponds to the same POJO class, + * check field's {@link JsonProperty} has flattening dots in it if so + * flatten the nested child JsonNode corresponds to the field in the given JsonNode. + * + * @param classField the field in a POJO class + * @param jsonNode the json node corresponds to POJO class that field belongs to + */ + @SuppressWarnings("unchecked") + private static void handleFlatteningForField(Field classField, JsonNode jsonNode) { + final JsonProperty jsonProperty = classField.getAnnotation(JsonProperty.class); + if (jsonProperty != null) { + final String jsonPropValue = jsonProperty.value(); + if (containsFlatteningDots(jsonPropValue)) { + JsonNode childJsonNode = findNestedNode(jsonNode, jsonPropValue); + ((ObjectNode) jsonNode).set(jsonPropValue, childJsonNode); + } + } + } + + /** + * Given a json node, find a nested node using given composed key. + * + * @param jsonNode the parent json node + * @param composedKey a key combines multiple keys using flattening dots. + * Flattening dots are dot character '.' those are not preceded by slash '\' + * Each flattening dot represents a level with following key as field key in that level + * @return nested json node located using given composed key + */ + private static JsonNode findNestedNode(JsonNode jsonNode, String composedKey) { + String[] jsonNodeKeys = splitKeyByFlatteningDots(composedKey); + for (String jsonNodeKey : jsonNodeKeys) { + jsonNode = jsonNode.get(unescapeEscapedDots(jsonNodeKey)); + if (jsonNode == null) { + return null; + } + } + return jsonNode; + } + + /** + * Checks whether the given key has flattening dots in it. + * Flattening dots are dot character '.' those are not preceded by slash '\' + * + * @param key the key + * @return true if the key has flattening dots, false otherwise. + */ + private static boolean containsFlatteningDots(String key) { + return key.matches(".+[^\\\\]\\..+"); + } + + /** + * Split the key by flattening dots. + * Flattening dots are dot character '.' those are not preceded by slash '\' + * + * @param key the key to split + * @return the array of sub keys + */ + private static String[] splitKeyByFlatteningDots(String key) { + return key.split("((? 0 && str.contains("."); + } + + /** + * Create a JsonParser for a given json node. + * @param jsonNode the json node + * @return the json parser + * @throws IOException + */ + private static JsonParser newJsonParserForNode(JsonNode jsonNode) throws IOException { + JsonParser parser = new JsonFactory().createParser(jsonNode.toString()); + parser.nextToken(); + return parser; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/FlatteningSerializer.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/FlatteningSerializer.java new file mode 100644 index 000000000..13bfc5b9e --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/FlatteningSerializer.java @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.BeanSerializer; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.ResolvableSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.google.common.collect.Sets; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Custom serializer for serializing types with wrapped properties. + * For example, a property with annotation @JsonProperty(value = "properties.name") + * will be mapped from a top level "name" property in the POJO model to + * {'properties' : { 'name' : 'my_name' }} in the serialized payload. + */ +public class FlatteningSerializer extends StdSerializer implements ResolvableSerializer { + /** + * The default mapperAdapter for the current type. + */ + private final JsonSerializer defaultSerializer; + + /** + * The object mapper for default serializations. + */ + private final ObjectMapper mapper; + + /** + * Creates an instance of FlatteningSerializer. + * @param vc handled type + * @param defaultSerializer the default JSON serializer + * @param mapper the object mapper for default serializations + */ + protected FlatteningSerializer(Class vc, JsonSerializer defaultSerializer, ObjectMapper mapper) { + super(vc, false); + this.defaultSerializer = defaultSerializer; + this.mapper = mapper; + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @param mapper the object mapper for default serializations + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule(final ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.setSerializerModifier(new BeanSerializerModifier() { + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer) { + if (BeanSerializer.class.isAssignableFrom(serializer.getClass())) { + return new FlatteningSerializer(beanDesc.getBeanClass(), serializer, mapper); + } + return serializer; + } + }); + return module; + } + + private List getAllDeclaredFields(Class clazz) { + List fields = new ArrayList<>(); + while (clazz != null && !clazz.equals(Object.class)) { + for (Field f : clazz.getDeclaredFields()) { + int mod = f.getModifiers(); + if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod)) { + fields.add(f); + } + } + clazz = clazz.getSuperclass(); + } + return fields; + } + + @SuppressWarnings("unchecked") + private void escapeMapKeys(Object value) { + if (value == null) { + return; + } + + if (value.getClass().isPrimitive() + || value.getClass().isEnum() + || value instanceof OffsetDateTime + || value instanceof ZonedDateTime + || value instanceof String + || value instanceof Period) { + return; + } + + int mod = value.getClass().getModifiers(); + if (Modifier.isFinal(mod) || Modifier.isStatic(mod)) { + return; + } + + if (value instanceof List) { + for (Object val : (List) value) { + escapeMapKeys(val); + } + return; + } + + if (value instanceof Map) { + for (String key : Sets.newHashSet(((Map) value).keySet())) { + if (key.contains(".")) { + String newKey = key.replaceAll("((?) value).remove(key); + ((Map) value).put(newKey, val); + } + } + for (Object val : ((Map) value).values()) { + escapeMapKeys(val); + } + return; + } + + for (Field f : getAllDeclaredFields(value.getClass())) { + f.setAccessible(true); + try { + escapeMapKeys(f.get(value)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + return; + } + escapeMapKeys(value); + // BFS for all collapsed properties + ObjectNode root = mapper.valueToTree(value); + ObjectNode res = root.deepCopy(); + Queue source = new LinkedBlockingQueue<>(); + Queue target = new LinkedBlockingQueue<>(); + source.add(root); + target.add(res); + while (!source.isEmpty()) { + ObjectNode current = source.poll(); + ObjectNode resCurrent = target.poll(); + Iterator> fields = current.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + ObjectNode node = resCurrent; + String key = field.getKey(); + JsonNode outNode = resCurrent.get(key); + if (key.matches(".+[^\\\\]\\..+")) { + // Handle flattening properties + // + String[] values = key.split("((? 0 + && (field.getValue()).get(0) instanceof ObjectNode) { + Iterator sourceIt = field.getValue().elements(); + Iterator targetIt = outNode.elements(); + while (sourceIt.hasNext()) { + source.add((ObjectNode) sourceIt.next()); + target.add((ObjectNode) targetIt.next()); + } + } + } + } + jgen.writeTree(res); + } + + @Override + public void resolve(SerializerProvider provider) throws JsonMappingException { + ((ResolvableSerializer) defaultSerializer).resolve(provider); + } + + @Override + public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSerializer) throws IOException { + serialize(value, gen, provider); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/HeadersSerializer.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/HeadersSerializer.java new file mode 100644 index 000000000..a92a0058e --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/HeadersSerializer.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import okhttp3.Headers; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Custom serializer for serializing {@link Headers} objects. + */ +public final class HeadersSerializer extends JsonSerializer { + /** + * Gets a module wrapping this serializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Headers.class, new HeadersSerializer()); + return module; + } + + @Override + public void serialize(Headers value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + Map headers = new HashMap<>(); + for (Map.Entry> entry : value.toMultimap().entrySet()) { + if (entry.getValue() != null && entry.getValue().size() == 1) { + headers.put(entry.getKey(), entry.getValue().get(0)); + } else if (entry.getValue() != null && entry.getValue().size() > 1) { + headers.put(entry.getKey(), entry.getValue()); + } + } + jgen.writeObject(headers); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JacksonAdapter.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JacksonAdapter.java new file mode 100644 index 000000000..c46ac4319 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JacksonAdapter.java @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.type.TypeBindings; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.microsoft.bot.restclient.CollectionFormat; +import com.microsoft.bot.restclient.protocol.SerializerAdapter; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * A serialization helper class wrapped around {@link JacksonConverterFactory} and {@link ObjectMapper}. + */ +public class JacksonAdapter implements SerializerAdapter { + /** + * An instance of {@link ObjectMapper} to serialize/deserialize objects. + */ + private final ObjectMapper mapper; + + /** + * An instance of {@link ObjectMapper} that does not do flattening. + */ + private final ObjectMapper simpleMapper; + + /** + * Creates a new JacksonAdapter instance with default mapper settings. + */ + public JacksonAdapter() { + simpleMapper = initializeObjectMapper(new ObjectMapper()); + ObjectMapper flatteningMapper = initializeObjectMapper(new ObjectMapper()) + .registerModule(FlatteningSerializer.getModule(simpleMapper())) + .registerModule(FlatteningDeserializer.getModule(simpleMapper())); + mapper = initializeObjectMapper(new ObjectMapper()) + // Order matters: must register in reverse order of hierarchy + .registerModule(AdditionalPropertiesSerializer.getModule(flatteningMapper)) + .registerModule(AdditionalPropertiesDeserializer.getModule(flatteningMapper)) + .registerModule(FlatteningSerializer.getModule(simpleMapper())) + .registerModule(FlatteningDeserializer.getModule(simpleMapper())); + } + + /** + * Gets a static instance of {@link ObjectMapper} that doesn't handle flattening. + * + * @return an instance of {@link ObjectMapper}. + */ + protected ObjectMapper simpleMapper() { + return simpleMapper; + } + + @Override + public ObjectMapper serializer() { + return mapper; + } + + @Override + public JacksonConverterFactory converterFactory() { + return JacksonConverterFactory.create(serializer()); + } + + @Override + public String serialize(Object object) throws IOException { + if (object == null) { + return null; + } + StringWriter writer = new StringWriter(); + serializer().writeValue(writer, object); + return writer.toString(); + } + + @Override + public String serializeRaw(Object object) { + if (object == null) { + return null; + } + try { + return CharMatcher.is('"').trimFrom(serialize(object)); + } catch (IOException ex) { + return null; + } + } + + @Override + public String serializeList(List list, CollectionFormat format) { + if (list == null) { + return null; + } + List serialized = new ArrayList<>(); + for (Object element : list) { + String raw = serializeRaw(element); + serialized.add(raw != null ? raw : ""); + } + return Joiner.on(format.getDelimiter()).join(serialized); + } + + private JavaType constructJavaType(final Type type) { + if (type instanceof ParameterizedType) { + JavaType[] javaTypeArgs = new JavaType[((ParameterizedType) type).getActualTypeArguments().length]; + for (int i = 0; i != ((ParameterizedType) type).getActualTypeArguments().length; ++i) { + javaTypeArgs[i] = constructJavaType(((ParameterizedType) type).getActualTypeArguments()[i]); + } + return mapper.getTypeFactory().constructType(type, + TypeBindings.create((Class) ((ParameterizedType) type).getRawType(), javaTypeArgs)); + } else { + return mapper.getTypeFactory().constructType(type); + } + } + + @Override + @SuppressWarnings("unchecked") + public T deserialize(String value, final Type type) throws IOException { + if (value == null || value.isEmpty()) { + return null; + } + return serializer().readValue(value, constructJavaType(type)); + } + + /** + * Initializes an instance of JacksonMapperAdapter with default configurations + * applied to the object mapper. + * + * @param mapper the object mapper to use. + */ + private static ObjectMapper initializeObjectMapper(ObjectMapper mapper) { + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .registerModule(new ParameterNamesModule()) + .registerModule(ByteArraySerializer.getModule()) + .registerModule(Base64UrlSerializer.getModule()) + .registerModule(HeadersSerializer.getModule()); + mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() + .withFieldVisibility(JsonAutoDetect.Visibility.ANY) + .withSetterVisibility(JsonAutoDetect.Visibility.NONE) + .withGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); + return mapper; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JacksonConverterFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JacksonConverterFactory.java new file mode 100644 index 000000000..45eea1321 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JacksonConverterFactory.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +import java.io.IOException; +import java.io.Reader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * A similar implementation of {@link retrofit2.converter.jackson.JacksonConverterFactory} which supports polymorphism. + */ +final class JacksonConverterFactory extends Converter.Factory { + /** + * Create an instance using {@code mapper} for conversion. + * + * @param mapper a user-provided {@link ObjectMapper} to use + * @return an instance of JacksonConverterFactory + */ + static JacksonConverterFactory create(ObjectMapper mapper) { + return new JacksonConverterFactory(mapper); + } + + /** + * The Jackson object mapper. + */ + private final ObjectMapper mapper; + + private JacksonConverterFactory(ObjectMapper mapper) { + if (mapper == null) { + throw new NullPointerException("mapper == null"); + } + this.mapper = mapper; + } + + @Override + public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { + JavaType javaType = mapper.getTypeFactory().constructType(type); + ObjectReader reader = mapper.readerFor(javaType); + return new JacksonResponseBodyConverter<>(reader); + } + + @Override + public Converter requestBodyConverter(Type type, + Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { + ObjectWriter writer = mapper.writer(); + return new JacksonRequestBodyConverter<>(writer); + } + + /** + * An instance of this class converts an object into JSON. + * + * @param type of request object + */ + static final class JacksonRequestBodyConverter implements Converter { + /** Jackson object writer. */ + private final ObjectWriter adapter; + + JacksonRequestBodyConverter(ObjectWriter adapter) { + this.adapter = adapter; + } + + @Override public RequestBody convert(T value) throws IOException { + byte[] bytes = adapter.writeValueAsBytes(value); + return RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), bytes); + } + } + + /** + * An instance of this class converts a JSON payload into an object. + * + * @param the expected object type to convert to + */ + static final class JacksonResponseBodyConverter implements Converter { + /** Jackson object reader. */ + private final ObjectReader adapter; + + JacksonResponseBodyConverter(ObjectReader adapter) { + this.adapter = adapter; + } + + @Override public T convert(ResponseBody value) throws IOException { + try (Reader reader = value.charStream()) { + return adapter.readValue(reader); + } + } + } +} + diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JsonFlatten.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JsonFlatten.java new file mode 100644 index 000000000..05ed2a10c --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/JsonFlatten.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient.serializer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used for flattening properties separated by '.'. + * E.g. a property with JsonProperty value "properties.value" + * will have "value" property under the "properties" tree on + * the wire. + * + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonFlatten { +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/package-info.java new file mode 100644 index 000000000..ceedba31d --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/serializer/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * The package contains classes that handle serialization and deserialization + * for the REST call payloads. + */ +package com.microsoft.bot.restclient.serializer; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AllowedCallersClaimsValidationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AllowedCallersClaimsValidationTests.java new file mode 100644 index 000000000..4df7754dc --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AllowedCallersClaimsValidationTests.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.connector; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.microsoft.bot.connector.authentication.AllowedCallersClaimsValidator; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; + +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Assert; +import org.junit.Test; + +public class AllowedCallersClaimsValidationTests { + + private final String version = "1.0"; + + private final String audienceClaim = UUID.randomUUID().toString(); + + public static List>> getConfigureServicesSucceedsData() { + String primaryAppId = UUID.randomUUID().toString(); + String secondaryAppId = UUID.randomUUID().toString(); + + List>> resultList = new ArrayList>>(); + // Null allowed callers + resultList.add(Pair.of(null, null)); + // Null configuration with attempted caller + resultList.add(Pair.of(primaryAppId, null)); + // Empty allowed callers array + resultList.add(Pair.of(null, new ArrayList())); + // Allow any caller + ArrayList anyCaller = new ArrayList(); + anyCaller.add("*"); + resultList.add(Pair.of(primaryAppId, anyCaller)); + // Specify allowed caller + ArrayList allowedCaller = new ArrayList(); + allowedCaller.add(primaryAppId); + resultList.add((Pair.of(primaryAppId, allowedCaller))); + // Specify multiple callers + ArrayList multipleCallers = new ArrayList(); + multipleCallers.add(primaryAppId); + multipleCallers.add(secondaryAppId); + resultList.add((Pair.of(primaryAppId, multipleCallers))); + // Blocked caller throws exception + ArrayList blockedCallers = new ArrayList(); + blockedCallers.add(secondaryAppId); + resultList.add((Pair.of(primaryAppId, blockedCallers))); + return resultList; + } + + @Test + public void TestAcceptAllowedCallersArray() { + List>> configuredServices = getConfigureServicesSucceedsData(); + for (Pair> item : configuredServices) { + acceptAllowedCallersArray(item.getLeft(), item.getRight()); + } + } + + + public void acceptAllowedCallersArray(String allowedCallerClaimId, List allowList) { + AllowedCallersClaimsValidator validator = new AllowedCallersClaimsValidator(allowList); + + if (allowedCallerClaimId != null) { + Map claims = createCallerClaims(allowedCallerClaimId); + + if (allowList != null) { + if (allowList.contains(allowedCallerClaimId) || allowList.contains("*")) { + validator.validateClaims(claims); + } else { + validateUnauthorizedAccessException(allowedCallerClaimId, validator, claims); + } + } else { + validateUnauthorizedAccessException(allowedCallerClaimId, validator, claims); + } + } + } + + private static void validateUnauthorizedAccessException( + String allowedCallerClaimId, + AllowedCallersClaimsValidator validator, + Map claims) { + try { + validator.validateClaims(claims); + } catch (RuntimeException exception) { + Assert.assertTrue(exception.getMessage().contains(allowedCallerClaimId)); + } + } + + private Map createCallerClaims(String appId) { + Map callerClaimMap = new HashMap(); + + callerClaimMap.put(AuthenticationConstants.APPID_CLAIM, appId); + callerClaimMap.put(AuthenticationConstants.VERSION_CLAIM, version); + callerClaimMap.put(AuthenticationConstants.AUDIENCE_CLAIM, audienceClaim); + return callerClaimMap; + } +} + diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AppCredentialsTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AppCredentialsTests.java new file mode 100644 index 000000000..6310b6597 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AppCredentialsTests.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.connector; + +import java.io.IOException; +import java.net.MalformedURLException; + +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.AppCredentialsInterceptor; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.Authenticator; +import com.microsoft.bot.restclient.ServiceClient; + +import org.junit.Assert; +import org.junit.Test; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import retrofit2.Retrofit; + +public class AppCredentialsTests { + + @Test + public void ConstructorTests() { + TestAppCredentials shouldDefaultToChannelScope = new TestAppCredentials("irrelevant"); + Assert.assertEquals(AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + shouldDefaultToChannelScope.oAuthScope()); + + TestAppCredentials shouldDefaultToCustomScope = new TestAppCredentials("irrelevant", "customScope"); + Assert.assertEquals("customScope", shouldDefaultToCustomScope.oAuthScope()); + } + + @Test + public void basicCredentialsTest() throws Exception { + TestAppCredentials credentials = new TestAppCredentials("irrelevant", "pass"); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + credentials.applyCredentialsFilter(clientBuilder); + clientBuilder.addInterceptor( + new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("Authorization"); + Assert.assertNull(header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient().newCall( + new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + private class TestAppCredentials extends AppCredentials { + TestAppCredentials(String channelAuthTenant) { + super(channelAuthTenant); + } + + TestAppCredentials(String channelAuthTenant, String oAuthScope) { + super(channelAuthTenant, oAuthScope); + } + + @Override + protected Authenticator buildAuthenticator() throws MalformedURLException { + return null; + } + + /** + * Apply the credentials to the HTTP request. + * + *

+ * Note: Provides the same functionality as dotnet ProcessHttpRequestAsync + *

+ * + * @param clientBuilder the builder for building up an {@link OkHttpClient} + */ + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.interceptors().add(new AppCredentialsInterceptor(this)); + } + + } +} + diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AsyncTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AsyncTests.java new file mode 100644 index 000000000..bd6757831 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AsyncTests.java @@ -0,0 +1,38 @@ +package com.microsoft.bot.connector; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.junit.Assert; +import org.junit.Test; + +public class AsyncTests { + @Test() + public void AsyncTryCompletionShouldCompleteExceptionally() { + CompletableFuture result = Async.tryCompletable(() -> { + throw new IllegalArgumentException("test"); + }); + + Assert.assertTrue(result.isCompletedExceptionally()); + } + + @Test + public void AsyncTryCompletionShouldComplete() { + CompletableFuture result = Async.tryCompletable(() -> CompletableFuture.completedFuture(true)); + Assert.assertTrue(result.join()); + } + + @Test + public void AsyncWrapBlockShouldCompleteExceptionally() { + CompletableFuture result = Async.wrapBlock(() -> { + throw new IllegalArgumentException("test"); + }); + + Assert.assertTrue(result.isCompletedExceptionally()); + } + + @Test + public void AsyncWrapBlockShouldComplete() { + CompletableFuture result = Async.wrapBlock(() -> true); + Assert.assertTrue(result.join()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AttachmentsTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AttachmentsTest.java index 8a69ca972..54055707f 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AttachmentsTest.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/AttachmentsTest.java @@ -1,6 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; -import com.microsoft.bot.schema.models.*; +import com.microsoft.bot.schema.*; import org.junit.Assert; import org.junit.Test; @@ -13,22 +16,22 @@ public class AttachmentsTest extends BotConnectorTestBase { @Test public void GetAttachmentInfo() { - AttachmentData attachment = new AttachmentData() - .withName("bot-framework.png") - .withType("image/png") - .withOriginalBase64(encodeToBase64(new File(getClass().getClassLoader().getResource("bot-framework.png").getFile()))); + AttachmentData attachment = new AttachmentData(); + attachment.setName("bot-framework.png"); + attachment.setType("image/png"); + attachment.setOriginalBase64(encodeToBase64(new File(getClass().getClassLoader().getResource("bot-framework.png").getFile()))); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse attachmentResponse = connector.conversations().uploadAttachment(conversation.id(), attachment); + ResourceResponse attachmentResponse = connector.getConversations().uploadAttachment(conversation.getId(), attachment).join(); - AttachmentInfo response = connector.attachments().getAttachmentInfo(attachmentResponse.id()); + AttachmentInfo response = connector.getAttachments().getAttachmentInfo(attachmentResponse.getId()).join(); - Assert.assertEquals(attachment.name(), response.name()); + Assert.assertEquals(attachment.getName(), response.getName()); } @Test @@ -44,23 +47,23 @@ public void GetAttachment() { e.printStackTrace(); } - AttachmentData attachment = new AttachmentData() - .withName("bot_icon.png") - .withType("image/png") - .withOriginalBase64(attachmentPayload); + AttachmentData attachment = new AttachmentData(); + attachment.setName("bot_icon.png"); + attachment.setType("image/png"); + attachment.setOriginalBase64(attachmentPayload); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse attachmentResponse = connector.conversations().uploadAttachment(conversation.id(), attachment); + ResourceResponse attachmentResponse = connector.getConversations().uploadAttachment(conversation.getId(), attachment).join(); - AttachmentInfo attachmentInfo = connector.attachments().getAttachmentInfo(attachmentResponse.id()); + AttachmentInfo attachmentInfo = connector.getAttachments().getAttachmentInfo(attachmentResponse.getId()).join(); - for (AttachmentView attView : attachmentInfo.views()) { - InputStream retrievedAttachment = connector.attachments().getAttachment(attachmentResponse.id(), attView.viewId()); + for (AttachmentView attView : attachmentInfo.getViews()) { + InputStream retrievedAttachment = connector.getAttachments().getAttachment(attachmentResponse.getId(), attView.getViewId()).join(); Assert.assertTrue(isSame(retrievedAttachment, attachmentStream)); } @@ -69,7 +72,7 @@ public void GetAttachment() { private byte[] encodeToBase64(File file) { try { FileInputStream fis = new FileInputStream(file); - byte[] result = new byte[(int)file.length()]; + byte[] result = new byte[(int) file.length()]; int size = fis.read(result); return result; } catch (Exception ex) { diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java index 2f62336b7..459c18799 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAccessTokenStub.java @@ -1,19 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.schema.models.TokenResponse; -import com.microsoft.rest.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; import okhttp3.OkHttpClient; -import okhttp3.Response; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -public class BotAccessTokenStub extends MicrosoftAppCredentials { +public class BotAccessTokenStub implements ServiceClientCredentials { private final String token; - public BotAccessTokenStub(String token, String appId, String appSecret) { - super(appId,appSecret); + public BotAccessTokenStub(String token) { this.token = token; } @@ -26,8 +22,4 @@ public BotAccessTokenStub(String token, String appId, String appSecret) { public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { clientBuilder.interceptors().add(new TestBearerTokenInterceptor(this.token)); } - - - - } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAuthenticatorTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAuthenticatorTest.java deleted file mode 100644 index f8108fd3e..000000000 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotAuthenticatorTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.microsoft.bot.connector; - -import com.microsoft.aad.adal4j.AuthenticationException; -import com.microsoft.bot.connector.authentication.*; -import com.microsoft.bot.schema.models.Activity; -import okhttp3.Request; -import org.junit.Assert; -import org.junit.Test; -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -public class BotAuthenticatorTest { - - private static final String AppId = "2cd87869-38a0-4182-9251-d056e8f0ac24"; - private static final String AppPassword = "2.30Vs3VQLKt974F"; - - @Test - public void ConnectorAuthHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl(AppId, ""); - ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, "", "https://webchat.botframework.com/").get(); - - Assert.assertTrue(identity.isAuthenticated()); - } - - @Test - public void ConnectorAuthHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("00000000-0000-0000-0000-000000000000", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (AuthenticationException e) { - Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token")); - } - } - - @Test - public void ConnectorAuthHeaderBotWithNoCredentialsShouldNotValidate() throws IOException, ExecutionException, InterruptedException { - // token received and auth disabled - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (AuthenticationException e) { - Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token")); - } - } - - @Test - public void EmptyHeaderBotWithNoCredentialsShouldThrow() throws ExecutionException, InterruptedException { - String header = ""; - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (IllegalArgumentException e) { - Assert.assertTrue(e.getMessage().contains("authHeader")); - } - } - - @Test - public void EmulatorMsaHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl(AppId, ""); - ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, "", "https://webchat.botframework.com/").get(); - - Assert.assertTrue(identity.isAuthenticated()); - } - - @Test - public void EmulatorMsaHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("00000000-0000-0000-0000-000000000000", ""); - - try { - JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get(); - } catch (AuthenticationException e) { - Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token")); - } - } - - /** - * Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list. - */ - @Test - public void ChannelMsaHeaderValidServiceUrlShouldBeTrusted() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl(AppId, ""); - JwtTokenValidation.authenticateRequest( - new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"), - header, - credentials); - - Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/")); - } - - /** - * Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list. - */ - @Test - public void ChannelMsaHeaderInvalidServiceUrlShouldNotBeTrusted() throws IOException, ExecutionException, InterruptedException { - String header = getHeaderToken(); - CredentialProvider credentials = new CredentialProviderImpl("7f74513e-6f96-4dbc-be9d-9a81fea22b88", ""); - - try { - JwtTokenValidation.authenticateRequest( - new Activity().withServiceUrl("https://webchat.botframework.com/"), - header, - credentials); - Assert.fail("Should have thrown AuthenticationException"); - } catch (AuthenticationException ex) { - Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/")); - } - - } - - /** - * Tests with no authentication header and makes sure the service URL is not added to the trusted list. - */ - @Test - public void ChannelAuthenticationDisabledShouldBeAnonymous() throws ExecutionException, InterruptedException { - String header = ""; - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(new Activity().withServiceUrl("https://webchat.botframework.com/"), header, credentials).get(); - Assert.assertEquals("anonymous", identity.getIssuer()); - } - - /** - * Tests with no authentication header and makes sure the service URL is not added to the trusted list. - */ - @Test - public void ChannelAuthenticationDisabledServiceUrlShouldNotBeTrusted() throws ExecutionException, InterruptedException { - String header = ""; - CredentialProvider credentials = new CredentialProviderImpl("", ""); - - ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(new Activity().withServiceUrl("https://webchat.botframework.com/"), header, credentials).get(); - Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/")); - } - - private static String getHeaderToken() throws IOException { - Request request = new Request.Builder().url(AuthenticationConstants.ToChannelFromBotLoginUrl).build(); - return String.format("Bearer %s", new MicrosoftAppCredentials(AppId, AppPassword).getToken(request)); - } -} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java index bc8e41aa3..d99652b30 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotConnectorTestBase.java @@ -1,12 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; import com.microsoft.bot.connector.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.rest.RestClient; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.restclient.RestClient; public class BotConnectorTestBase extends TestBase { - protected ConnectorClientImpl connector; + protected ConnectorClient connector; protected ChannelAccount bot; protected ChannelAccount user; @@ -20,9 +23,9 @@ public BotConnectorTestBase(RunCondition runCondition) { @Override protected void initializeClients(RestClient restClient, String botId, String userId) { - connector = new ConnectorClientImpl(restClient); - bot = new ChannelAccount().withId(botId); - user = new ChannelAccount().withId(userId); + connector = new RestConnectorClient(restClient); + bot = new ChannelAccount(botId); + user = new ChannelAccount(userId); } @Override diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java index fdc6b5eef..416eeb6bd 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/ConversationsTest.java @@ -1,120 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; -import com.microsoft.bot.connector.models.ErrorResponseException; -import com.microsoft.bot.schema.models.*; +import com.microsoft.bot.connector.rest.ErrorResponseException; +import com.microsoft.bot.schema.*; import org.junit.Assert; import org.junit.Test; import java.io.File; import java.io.FileInputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletionException; public class ConversationsTest extends BotConnectorTestBase { @Test public void CreateConversation() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Create Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Create Conversation"); - ConversationParameters params = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot) - .withActivity(activity); + ConversationParameters params = new ConversationParameters(); + params.setMembers(Collections.singletonList(user)); + params.setBot(bot); + params.setActivity(activity); - ConversationResourceResponse result = connector.conversations().createConversation(params); + ConversationResourceResponse result = connector.getConversations().createConversation(params).join(); - Assert.assertNotNull(result.activityId()); + Assert.assertNotNull(result.getActivityId()); } @Test public void CreateConversationWithInvalidBot() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Create Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Create Conversation"); - ConversationParameters params = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot.withId("invalid-id")) - .withActivity(activity); + bot.setId("invalid-id"); + ConversationParameters params = new ConversationParameters(); + params.setMembers(Collections.singletonList(user)); + params.setBot(bot); + params.setActivity(activity); try { - ConversationResourceResponse result = connector.conversations().createConversation(params); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().startsWith("Invalid userId")); + ConversationResourceResponse result = connector.getConversations().createConversation(params).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().startsWith("Invalid userId")); + } else { + throw e; + } } } @Test public void CreateConversationWithoutMembers() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Create Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Create Conversation"); - ConversationParameters params = new ConversationParameters() - .withMembers(Collections.emptyList()) - .withBot(bot) - .withActivity(activity); + ConversationParameters params = new ConversationParameters(); + params.setMembers(Collections.emptyList()); + params.setBot(bot); + params.setActivity(activity); try { - ConversationResourceResponse result = connector.conversations().createConversation(params); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("BadArgument", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().startsWith("Conversations")); + ConversationResourceResponse result = connector.getConversations().createConversation(params).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("BadArgument", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().startsWith("Conversations")); + } else { + throw e; + } } } @Test public void CreateConversationWithBotMember() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Create Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Create Conversation"); - ConversationParameters params = new ConversationParameters() - .withMembers(Collections.singletonList(bot)) - .withBot(bot) - .withActivity(activity); + ConversationParameters params = new ConversationParameters(); + params.setMembers(Collections.singletonList(bot)); + params.setBot(bot); + params.setActivity(activity); try { - ConversationResourceResponse result = connector.conversations().createConversation(params); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("BadArgument", e.body().error().code().toString()); + ConversationResourceResponse result = connector.getConversations().createConversation(params).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertEquals("BadArgument", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + } + } + + @Test + public void CreateConversationWithNullParameter() { + try { + ConversationResourceResponse result = connector.getConversations().createConversation(null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); } } @Test public void GetConversationMembers() { - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - List members = connector.conversations().getConversationMembers(conversation.id()); + List members = connector.getConversations().getConversationMembers(conversation.getId()).join(); boolean hasUser = false; for (ChannelAccount member : members) { - hasUser = member.id().equals(user.id()); + hasUser = member.getId().equals(user.getId()); if (hasUser) break; } @@ -124,152 +144,248 @@ public void GetConversationMembers() { @Test public void GetConversationMembersWithInvalidConversationId() { - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); + + try { + List members = connector.getConversations().getConversationMembers(conversation.getId().concat("M")).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().contains("The specified channel was not found")); + } else { + throw e; + } + } + } + + @Test + public void GetConversationMembersWithNullConversationId() { + try { + List members = connector.getConversations().getConversationMembers(null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void GetConversationPagedMembers() { + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); try { - List members = connector.conversations().getConversationMembers(conversation.id().concat("M")); - Assert.fail("expected exception was not occurred."); + PagedMembersResult pagedMembers = connector.getConversations().getConversationPagedMembers(conversation.getId()).join(); + + boolean hasUser = false; + for (ChannelAccount member : pagedMembers.getMembers()) { + hasUser = member.getId().equalsIgnoreCase(user.getId()); + if (hasUser) + break; + } + + Assert.assertTrue(hasUser); } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().contains("The specified channel was not found")); + Assert.assertEquals("ServiceError", e.body().getError().getCode().toString()); + } + } + + @Test + public void GetConversationPagedMembersWithInvalidConversationId() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Get Activity Members"); + + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + createMessage.setActivity(activity); + + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); + + try { + connector.getConversations().getConversationPagedMembers(conversation.getId().concat("M")).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals(400, ((ErrorResponseException)e.getCause()).response().code()); + } else { + throw e; + } } } @Test public void SendToConversation() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withName("activity") - .withText("TEST Send to Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setName("activity"); + activity.setText("TEST Send to Conversation"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); - Assert.assertNotNull(response.id()); + Assert.assertNotNull(response.getId()); } @Test public void SendToConversationWithInvalidConversationId() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withName("activity") - .withText("TEST Send to Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setName("activity"); + activity.setText("TEST Send to Conversation"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); try { - ResourceResponse response = connector.conversations().sendToConversation(conversation.id().concat("M"), activity); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().contains("The specified channel was not found")); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId().concat("M"), activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().contains("The specified channel was not found")); + } else { + throw e; + } } } @Test public void SendToConversationWithInvalidBotId() { - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot.withId("B21S8SG7K:T03CWQ0QB")) - .withName("activity") - .withText("TEST Send to Conversation"); + bot.setId("B21S8SG7K:T03CWQ0QB"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setName("activity"); + activity.setText("TEST Send to Conversation"); try { - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("MissingProperty", e.body().error().code().toString()); - Assert.assertEquals("The bot referenced by the 'from' field is unrecognized", e.body().error().message()); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("MissingProperty", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertEquals("The bot referenced by the 'from' field is unrecognized", ((ErrorResponseException)e.getCause()).body().getError().getMessage()); + } else { + throw e; + } } } @Test - public void SendCardToConversation() { - - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withName("activity") - .withText("TEST Send Card to Conversation") - .withAttachments(Arrays.asList( - new Attachment() - .withContentType("application/vnd.microsoft.card.hero") - .withContent(new HeroCard() - .withTitle("A static image") - .withSubtitle("JPEG image") - .withImages(Collections.singletonList(new CardImage() - .withUrl("https://docs.microsoft.com/en-us/bot-framework/media/designing-bots/core/dialogs-screens.png")))), - new Attachment() - .withContentType("application/vnd.microsoft.card.hero") - .withContent(new HeroCard() - .withTitle("An animation") - .withSubtitle("GIF image") - .withImages(Collections.singletonList(new CardImage() - .withUrl("http://i.giphy.com/Ki55RUbOV5njy.gif")))) + public void SendToConversationWithNullConversationId() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Send to Conversation with null conversation id"); - )); + try { + connector.getConversations().sendToConversation(null, activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + @Test + public void SendToConversationWithNullActivity() { + try { + connector.getConversations().sendToConversation("id",null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); + @Test + public void SendCardToConversation() { - Assert.assertNotNull(response.id()); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setName("activity"); + activity.setText("TEST Send Card to Conversation"); + CardImage imageJPEG = new CardImage(); + imageJPEG.setUrl("https://docs.microsoft.com/en-us/bot-framework/media/designing-bots/core/dialogs-screens.png"); + HeroCard heroCardJPEG = new HeroCard(); + heroCardJPEG.setTitle("A static image"); + heroCardJPEG.setSubtitle("JPEG image"); + heroCardJPEG.setImages(Collections.singletonList(imageJPEG)); + Attachment attachmentJPEG = new Attachment(); + attachmentJPEG.setContentType("application/vnd.microsoft.card.hero"); + attachmentJPEG.setContent(heroCardJPEG); + + CardImage imageGIF = new CardImage(); + imageGIF.setUrl("http://i.giphy.com/Ki55RUbOV5njy.gif"); + HeroCard heroCardGIF = new HeroCard(); + heroCardGIF.setTitle("An animation"); + heroCardGIF.setSubtitle("GIF image"); + heroCardGIF.setImages(Collections.singletonList(imageGIF)); + Attachment attachmentGIF = new Attachment(); + attachmentGIF.setContentType("application/vnd.microsoft.card.hero"); + attachmentGIF.setContent(heroCardGIF); + activity.setAttachments(Arrays.asList(attachmentJPEG, attachmentGIF)); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); + + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); + + Assert.assertNotNull(response.getId()); } @Test public void GetActivityMembers() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Get Activity Members"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Get Activity Members"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot) - .withActivity(activity); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + createMessage.setActivity(activity); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - List members = connector.conversations().getActivityMembers(conversation.id(), conversation.activityId()); + List members = connector.getConversations().getActivityMembers(conversation.getId(), conversation.getActivityId()).join(); boolean hasUser = false; for (ChannelAccount member : members) { - hasUser = member.id().equals(user.id()); + hasUser = member.getId().equals(user.getId()); if (hasUser) break; } @@ -279,211 +395,361 @@ public void GetActivityMembers() { @Test public void GetActivityMembersWithInvalidConversationId() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Get Activity Members"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Get Activity Members"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot) - .withActivity(activity); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + createMessage.setActivity(activity); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); try { - List members = connector.conversations().getActivityMembers(conversation.id().concat("M"), conversation.activityId()); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().contains("The specified channel was not found")); + List members = connector.getConversations().getActivityMembers(conversation.getId().concat("M"), conversation.getActivityId()).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().contains("The specified channel was not found")); + } else { + throw e; + } + } + } + + @Test + public void GetActivityMembersWithNullConversationId() { + try { + connector.getConversations().getActivityMembers(null, "id").join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void GetActivityMembersWithNullActivityId() { + try { + connector.getConversations().getActivityMembers("id", null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); } } @Test public void ReplyToActivity() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Send to Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Send to Conversation"); - Activity reply = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Reply to Activity"); + Activity reply = new Activity(ActivityTypes.MESSAGE); + reply.setRecipient(user); + reply.setFrom(bot); + reply.setText("TEST Reply to Activity"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); - ResourceResponse replyResponse = connector.conversations().replyToActivity(conversation.id(), response.id(), reply); + ResourceResponse replyResponse = connector.getConversations().replyToActivity(conversation.getId(), response.getId(), reply).join(); - Assert.assertNotNull(replyResponse.id()); + Assert.assertNotNull(replyResponse.getId()); } @Test public void ReplyToActivityWithInvalidConversationId() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Send to Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Send to Conversation"); - Activity reply = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Reply to Activity"); + Activity reply = new Activity(ActivityTypes.MESSAGE); + reply.setRecipient(user); + reply.setFrom(bot); + reply.setText("TEST Reply to Activity"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); try { - ResourceResponse replyResponse = connector.conversations().replyToActivity(conversation.id().concat("M"), response.id(), reply); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().contains("The specified channel was not found")); + ResourceResponse replyResponse = connector.getConversations().replyToActivity(conversation.getId().concat("M"), response.getId(), reply).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().contains("The specified channel was not found")); + } else { + throw e; + } + } + } + + @Test + public void ReplyToActivityWithNullConversationId() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Reply activity with null conversation id"); + + try { + connector.getConversations().replyToActivity(null, "id", activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void ReplyToActivityWithNullActivityId() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Reply activity with null activity id"); + + try { + connector.getConversations().replyToActivity("id", null, activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void ReplyToActivityWithNullActivity() { + try { + connector.getConversations().replyToActivity("id", "id", null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void ReplyToActivityWithNullReply() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Reply activity with null reply"); + + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); + + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); + + try { + ResourceResponse replyResponse = connector.getConversations().replyToActivity(conversation.getId(), response.getId(), null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); } } @Test public void DeleteActivity() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Delete Activity"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Delete Activity"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot) - .withActivity(activity); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + createMessage.setActivity(activity); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - connector.conversations().deleteActivity(conversation.id(), conversation.activityId()); + connector.getConversations().deleteActivity(conversation.getId(), conversation.getActivityId()); - Assert.assertNotNull(conversation.activityId()); + Assert.assertNotNull(conversation.getActivityId()); } @Test public void DeleteActivityWithInvalidConversationId() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Delete Activity"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Delete Activity"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot) - .withActivity(activity); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); + createMessage.setActivity(activity); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); try { - connector.conversations().deleteActivity("B21S8SG7K:T03CWQ0QB", conversation.activityId()); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().contains("Invalid ConversationId")); + connector.getConversations().deleteActivity("B21S8SG7K:T03CWQ0QB", conversation.getActivityId()).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().contains("Invalid ConversationId")); + } else { + throw e; + } + } + } + + @Test + public void DeleteActivityWithNullConversationId() { + try { + connector.getConversations().deleteActivity(null, "id").join(); + Assert.fail("expected exception did not occur."); + } catch(CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void DeleteActivityWithNullActivityId() { + try { + connector.getConversations().deleteActivity("id", null).join(); + Assert.fail("expected exception did not occur."); + } catch(CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); } } @Test public void UpdateActivity() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Send to Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Send to Conversation"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); - Activity update = activity.withId(response.id()) - .withText("TEST Update Activity"); + activity.setId(response.getId()); + activity.setText("TEST Update Activity"); - ResourceResponse updateResponse = connector.conversations().updateActivity(conversation.id(), response.id(), update); + ResourceResponse updateResponse = connector.getConversations().updateActivity(conversation.getId(), response.getId(), activity).join(); - Assert.assertNotNull(updateResponse.id()); + Assert.assertNotNull(updateResponse.getId()); } @Test public void UpdateActivityWithInvalidConversationId() { - Activity activity = new Activity() - .withType(ActivityTypes.MESSAGE) - .withRecipient(user) - .withFrom(bot) - .withText("TEST Send to Conversation"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Send to Conversation"); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse response = connector.conversations().sendToConversation(conversation.id(), activity); + ResourceResponse response = connector.getConversations().sendToConversation(conversation.getId(), activity).join(); - Activity update = activity.withId(response.id()) - .withText("TEST Update Activity"); + activity.setId(response.getId()); + activity.setText("TEST Update Activity"); try { - ResourceResponse updateResponse = connector.conversations().updateActivity("B21S8SG7K:T03CWQ0QB", response.id(), update); - Assert.fail("expected exception was not occurred."); - } catch (ErrorResponseException e) { - Assert.assertEquals("ServiceError", e.body().error().code().toString()); - Assert.assertTrue(e.body().error().message().contains("Invalid ConversationId")); + ResourceResponse updateResponse = connector.getConversations().updateActivity("B21S8SG7K:T03CWQ0QB", response.getId(), activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + if (e.getCause() instanceof ErrorResponseException) { + Assert.assertEquals("ServiceError", ((ErrorResponseException)e.getCause()).body().getError().getCode()); + Assert.assertTrue(((ErrorResponseException)e.getCause()).body().getError().getMessage().contains("Invalid ConversationId")); + } else { + throw e; + } + } + } + + @Test + public void UpdateActivityWithNullConversationId() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Activity to be updated with null conversation Id"); + + try { + connector.getConversations().updateActivity(null, "id", activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void UpdateActivityWithNullActivityId() { + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setRecipient(user); + activity.setFrom(bot); + activity.setText("TEST Activity to be updated with null activity Id"); + + try { + connector.getConversations().updateActivity("id", null, activity).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); + } + } + + @Test + public void UpdateActivityWithNullActivity() { + try { + connector.getConversations().updateActivity("id", "id", null).join(); + Assert.fail("expected exception did not occur."); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("cannot be null")); } } @Test public void UploadAttachment() { - AttachmentData attachment = new AttachmentData() - .withName("bot-framework.png") - .withType("image/png") - .withOriginalBase64(encodeToBase64(new File(getClass().getClassLoader().getResource("bot-framework.png").getFile()))); + AttachmentData attachment = new AttachmentData(); + attachment.setName("bot-framework.png"); + attachment.setType("image/png"); + attachment.setOriginalBase64(encodeToBase64(new File(getClass().getClassLoader().getResource("bot-framework.png").getFile()))); - ConversationParameters createMessage = new ConversationParameters() - .withMembers(Collections.singletonList(user)) - .withBot(bot); + ConversationParameters createMessage = new ConversationParameters(); + createMessage.setMembers(Collections.singletonList(user)); + createMessage.setBot(bot); - ConversationResourceResponse conversation = connector.conversations().createConversation(createMessage); + ConversationResourceResponse conversation = connector.getConversations().createConversation(createMessage).join(); - ResourceResponse response = connector.conversations().uploadAttachment(conversation.id(), attachment); + ResourceResponse response = connector.getConversations().uploadAttachment(conversation.getId(), attachment).join(); - Assert.assertNotNull(response.id()); + Assert.assertNotNull(response.getId()); } private byte[] encodeToBase64(File file) { try { FileInputStream fis = new FileInputStream(file); - byte[] result = new byte[(int)file.length()]; + byte[] result = new byte[(int) file.length()]; int size = fis.read(result); return result; } catch (Exception ex) { diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EmulatorValidationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EmulatorValidationTests.java new file mode 100644 index 000000000..24e27839e --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EmulatorValidationTests.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.EmulatorValidation; +import org.junit.Assert; +import org.junit.Test; + +public class EmulatorValidationTests { + @Test + public void NoSchemeTokenIsNotFromEmulator() { + Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("AbCdEf123456")); + } + + @Test + public void OnePartTokenIsNotFromEmulator() { + Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("Bearer AbCdEf123456")); + } + + @Test + public void NoIssuerIsNotFromEmulator() { + Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdlIjoiSldUIFJ1bGVzISIsImlhdCI6MTQ1OTQ0ODExOSwiZXhwIjoxNDU5NDU0NTE5fQ.-yIVBD5b73C75osbmwwshQNRC7frWUYrqaTjTpza2y4")); + } + + @Test + public void ValidTokenSuccess() { + String emToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCIsImtpZCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCJ9.eyJhdWQiOiI5YzI4NmUyZi1lMDcwLTRhZjUtYTNmMS0zNTBkNjY2MjE0ZWQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTY2NDIyMTY3LCJuYmYiOjE1NjY0MjIxNjcsImV4cCI6MTU2NjQyNjA2NywiYWlvIjoiNDJGZ1lKaDBoK0VmRDAvNnVWaUx6NHZuL25UK0RnQT0iLCJhcHBpZCI6IjljMjg2ZTJmLWUwNzAtNGFmNS1hM2YxLTM1MGQ2NjYyMTRlZCIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJPUXNSLWExUlpFS2tJcG9seUNJUUFBIiwidmVyIjoiMS4wIn0.J9qHO11oZlrpDU3MJcTJe3ErUqj0kw-ZQioYKbkwZ7ZpAx5hl01BETts-LOaE14tImqYqM2K86ZyX5LuAp2snru9LJ4S6-cVZ1_lp_IY4r61UuUJRiVUzn25kRZEN-TFi8Aj1iyL-ueeNr52MM1Sr2UUH73fwrferH8_0qa1IYc7affhjlFEWxSte0SN7iT5WaYK32d_nsgzJdZiCMZJPCpG39U2FYnSI8q7vvYjNbp8wDJc46Q4Jdd3zXYRgHWRBGL_EEkzzk9IFpHN7WoVaqNtgMiA4Vf8bde3eAS5lBBtE5VZ0F6fG4Qeg6zjOAxPBZqvAASMpgyDlSQMknevOQ"; + Assert.assertTrue(EmulatorValidation.isTokenFromEmulator(emToken)); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java index 52875b9c2..b01fc078a 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/EndorsementsValidatorTests.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; import com.microsoft.bot.connector.authentication.EndorsementsValidator; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenExtractorTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenExtractorTests.java new file mode 100644 index 000000000..609b64843 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenExtractorTests.java @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.auth0.jwt.algorithms.Algorithm; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ChannelValidation; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.EmulatorValidation; +import com.microsoft.bot.connector.authentication.GovernmentChannelValidation; +import com.microsoft.bot.connector.authentication.JwtTokenExtractor; +import com.microsoft.bot.connector.authentication.OpenIdMetadata; +import com.microsoft.bot.connector.authentication.OpenIdMetadataKey; +import com.microsoft.bot.connector.authentication.TokenValidationParameters; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.junit.Before; +import org.junit.Test; + +/** + * Test Notes: + * + * The PKCS12 certificates were created using these steps: + * https://kb.globalscape.com/Knowledgebase/11039/Generating-a-PKCS12-Private-Key-and-Public-Certificate + * + * For the expired cert, just specify a negative number of days in step #4. + * + * For both valid and expired certs, these unit tests expect the alias for both to be "bot-connector-pkcs12" + * and the password to be "botframework" + */ +public class JwtTokenExtractorTests { + private CertInfo valid; + private CertInfo expired; + + @Before + public void setup() throws GeneralSecurityException, IOException { + ChannelValidation.getTokenValidationParameters().validateLifetime = false; + EmulatorValidation.getTokenValidationParameters().validateLifetime = false; + GovernmentChannelValidation.getTokenValidationParameters().validateLifetime = false; + + valid = loadCert("bot-connector.pkcs12"); + expired = loadCert("bot-connector-expired.pkcs12"); + } + + @Test(expected = CompletionException.class) + public void JwtTokenExtractor_WithExpiredCert_ShouldNotAllowCertSigningKey() { + // this should throw a CompletionException (which contains an AuthenticationException) + buildExtractorAndValidateToken( + expired.cert, expired.keypair.getPrivate() + ).join(); + } + + @Test + public void JwtTokenExtractor_WithValidCert_ShouldAllowCertSigningKey() { + // this should not throw + buildExtractorAndValidateToken( + valid.cert, valid.keypair.getPrivate() + ).join(); + } + + @Test(expected = CompletionException.class) + public void JwtTokenExtractor_WithExpiredToken_ShouldNotAllow() { + // this should throw a CompletionException (which contains an AuthenticationException) + Date now = new Date(); + Date issuedAt = new Date(now.getTime() - 86400000L); + + buildExtractorAndValidateToken( + expired.cert, expired.keypair.getPrivate(), issuedAt + ).join(); + } + + private CompletableFuture buildExtractorAndValidateToken( + X509Certificate cert, + PrivateKey privateKey + ) { + return buildExtractorAndValidateToken(cert, privateKey, new Date()); + } + + private CompletableFuture buildExtractorAndValidateToken( + X509Certificate cert, + PrivateKey privateKey, + Date issuedAt + ) { + TokenValidationParameters tokenValidationParameters = createTokenValidationParameters(cert); + + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + tokenValidationParameters, + "https://login.botframework.com/v1/.well-known/openidconfiguration", + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ); + + String token = createTokenForCertificate(cert, privateKey, issuedAt); + + return tokenExtractor.getIdentity("Bearer " + token, "test"); + } + + private static String createTokenForCertificate(X509Certificate cert, PrivateKey privateKey) { + return createTokenForCertificate(cert, privateKey, new Date()); + } + + // creates a token that expires 5 minutes from the 'issuedAt' value. + private static String createTokenForCertificate(X509Certificate cert, PrivateKey privateKey, Date issuedAt) { + RSAPublicKey publicKey = (RSAPublicKey) cert.getPublicKey(); + Algorithm algorithm = Algorithm.RSA256(publicKey, (RSAPrivateKey) privateKey); + return com.auth0.jwt.JWT.create() + .withIssuer("https://api.botframework.com") + .withIssuedAt(issuedAt) + .withNotBefore(issuedAt) + .withExpiresAt(new Date(issuedAt.getTime() + 300000L)) + .sign(algorithm); + } + + private static TokenValidationParameters createTokenValidationParameters(X509Certificate cert) + { + TokenValidationParameters parameters = new TokenValidationParameters(); + parameters.validateIssuer = false; + parameters.validIssuers = Collections.singletonList(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + + // Audience validation takes place in JwtTokenExtractor + parameters.validateAudience = false; + parameters.validateLifetime = true; + parameters.clockSkew = Duration.ofMinutes(5); + parameters.requireSignedTokens = true; + + // provide a custom resolver so that calls to openid won't happen (which wouldn't + // work for these tests). + parameters.issuerSigningKeyResolver = key -> (OpenIdMetadata) keyId -> { + // return our certificate data + OpenIdMetadataKey key1 = new OpenIdMetadataKey(); + key1.key = (RSAPublicKey) cert.getPublicKey(); + key1.certificateChain = Collections.singletonList(encodeCertificate(cert)); + return key1; + }; + return parameters; + } + + private static class CertInfo { + public X509Certificate cert; + public KeyPair keypair; + } + + private static CertInfo loadCert(String pkcs12File) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, + UnrecoverableKeyException { + InputStream fis = ClassLoader.getSystemResourceAsStream(pkcs12File); + KeyStore p12 = KeyStore.getInstance("pkcs12"); + p12.load(fis, "botframework".toCharArray()); + + CertInfo certInfo = new CertInfo(); + certInfo.cert = (X509Certificate) p12.getCertificate("bot-connector-pkcs12"); + certInfo.keypair = new KeyPair(certInfo.cert.getPublicKey(), + (PrivateKey) p12.getKey("bot-connector-pkcs12", "botframework".toCharArray()) + ); + return certInfo; + } + + private static String encodeCertificate(Certificate certificate) { + try { + Base64.Encoder encoder = Base64.getEncoder(); + byte[] rawCrtText = certificate.getEncoded(); + return new String(encoder.encode(rawCrtText)); + } catch(CertificateEncodingException e) { + return null; + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java new file mode 100644 index 000000000..09dd9f5ad --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenValidationTests.java @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.*; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.RoleTypes; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; + +public class JwtTokenValidationTests { + + private static final String APPID = "2cd87869-38a0-4182-9251-d056e8f0ac24"; + private static final String APPPASSWORD = "2.30Vs3VQLKt974F"; + + private static String getHeaderToken() { + return String.format("Bearer %s", new MicrosoftAppCredentials(APPID, APPPASSWORD).getToken().join()); + } + + private static String getGovHeaderToken() { + return String.format("Bearer %s", new MicrosoftGovernmentAppCredentials(APPID, APPPASSWORD).getToken().join()); + } + +// @Test +// public void ConnectorAuthHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { +// String header = getHeaderToken(); +// CredentialProvider credentials = new SimpleCredentialProvider(APPID, ""); +// ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader( +// header, +// credentials, +// new SimpleChannelProvider(), +// "", +// "https://webchat.botframework.com/").join(); +// +// Assert.assertTrue(identity.isAuthenticated()); +// } + +// @Test +// public void Connector_AuthHeader_CorrectAppIdAndServiceUrl_WithGovChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException { +// JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds( +// APPID, +// APPPASSWORD, +// GovernmentAuthenticationConstants.CHANNELSERVICE +// ); +// } + +// @Test +// public void ConnectorAuthHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { +// String header = getHeaderToken(); +// CredentialProvider credentials = new SimpleCredentialProvider("00000000-0000-0000-0000-000000000000", ""); +// +// try { +// JwtTokenValidation.validateAuthHeader( +// header, +// credentials, +// new SimpleChannelProvider(), +// "", +// null).join(); +// } catch (CompletionException e) { +// Assert.assertTrue(e.getCause() instanceof AuthenticationException); +// } +// } + +// @Test +// public void ConnectorAuthHeaderBotWithNoCredentialsShouldNotValidate() throws IOException, ExecutionException, InterruptedException { +// // token received and auth disabled +// String header = getHeaderToken(); +// CredentialProvider credentials = new SimpleCredentialProvider("", ""); +// +// try { +// JwtTokenValidation.validateAuthHeader( +// header, +// credentials, +// new SimpleChannelProvider(), +// "", +// null).join(); +// } catch (CompletionException e) { +// Assert.assertTrue(e.getCause() instanceof AuthenticationException); +// } +// } + + @Test + public void EmptyHeaderBotWithNoCredentialsShouldThrow() throws ExecutionException, InterruptedException { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + new SimpleChannelProvider(), + "", + null).join(); + Assert.fail("Should have thrown IllegalArgumentException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause().getMessage().contains("authHeader")); + } + } + +// @Test +// public void EmulatorMsaHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException { +// String header = getHeaderToken(); +// CredentialProvider credentials = new SimpleCredentialProvider(APPID, ""); +// ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader( +// header, +// credentials, +// new SimpleChannelProvider(), +// "", +// "https://webchat.botframework.com/").join(); +// +// Assert.assertTrue(identity.isAuthenticated()); +// } + +// @Test +// public void EmulatorMsaHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException { +// String header = getHeaderToken(); +// CredentialProvider credentials = new SimpleCredentialProvider("00000000-0000-0000-0000-000000000000", ""); +// +// try { +// JwtTokenValidation.validateAuthHeader( +// header, +// credentials, +// new SimpleChannelProvider(), +// "", +// null).join(); +// } catch (CompletionException e) { +// Assert.assertTrue(e.getCause() instanceof AuthenticationException); +// } +// } + +// @Test +// public void Emulator_AuthHeader_CorrectAppIdAndServiceUrl_WithGovChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException { +// JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds( +// "2cd87869-38a0-4182-9251-d056e8f0ac24", // emulator creds +// "2.30Vs3VQLKt974F", +// GovernmentAuthenticationConstants.CHANNELSERVICE); +// } + +// @Test +// public void Emulator_AuthHeader_CorrectAppIdAndServiceUrl_WithPrivateChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException { +// JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds( +// "2cd87869-38a0-4182-9251-d056e8f0ac24", // emulator creds +// "2.30Vs3VQLKt974F", +// "TheChannel"); +// } + + /** + * Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list. + */ +// @Test +// public void ChannelMsaHeaderInvalidServiceUrlShouldNotBeTrusted() throws IOException, ExecutionException, InterruptedException { +// String header = getHeaderToken(); +// CredentialProvider credentials = new SimpleCredentialProvider("7f74513e-6f96-4dbc-be9d-9a81fea22b88", ""); +// +// try { +// Activity activity = new Activity(ActivityTypes.MESSAGE); +// activity.setServiceUrl("https://webchat.botframework.com/"); +// JwtTokenValidation.authenticateRequest( +// activity, +// header, +// credentials, +// new SimpleChannelProvider()).join(); +// Assert.fail("Should have thrown AuthenticationException"); +// } catch (CompletionException e) { +// Assert.assertTrue(e.getCause() instanceof AuthenticationException); +// } +// } + + /** + * Tests with no authentication header and makes sure the service URL is not added to the trusted list. + */ + @Test + public void ChannelAuthenticationDisabledShouldBeAnonymous() throws ExecutionException, InterruptedException { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setServiceUrl("https://webchat.botframework.com/"); + ClaimsIdentity identity = JwtTokenValidation.authenticateRequest( + activity, + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.assertEquals("anonymous", identity.getIssuer()); + } + + /** + * Tests with no authentication header and makes sure the service URL is not added to the trusted list. + */ + @Test + public void ChannelAuthenticationDisabledAndSkillShouldBeAnonymous() throws ExecutionException, InterruptedException { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider("", ""); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setServiceUrl("https://webchat.botframework.com/"); + activity.setChannelId(Channels.EMULATOR); + activity.setRelatesTo(new ConversationReference()); + ChannelAccount skillAccount = new ChannelAccount(); + skillAccount.setRole(RoleTypes.SKILL); + activity.setRecipient(skillAccount); + ClaimsIdentity identity = JwtTokenValidation.authenticateRequest( + activity, + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, identity.getType()); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_SKILL_APPID, JwtTokenValidation.getAppIdFromClaims(identity.claims())); + } + + + @Test + public void ChannelNoHeaderAuthenticationEnabledShouldThrow() throws IOException, ExecutionException, InterruptedException { + try { + String header = ""; + CredentialProvider credentials = new SimpleCredentialProvider(APPID, APPPASSWORD); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"); + JwtTokenValidation.authenticateRequest( + activity, + header, + credentials, + new SimpleChannelProvider()).join(); + Assert.fail("Should have thrown AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_Succeeds() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + } catch (CompletionException e) { + Assert.fail("Should not have thrown " + e.getCause().getClass().getName()); + } + } + + @Test + public void EnterpriseChannelValidation_NoAuthentication_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(null, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoAudienceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, ""); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_WrongAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, "abc"); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoServiceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_NoServiceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, ""); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void EnterpriseChannelValidation_WrongServiceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, null); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, "other"); + ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an AuthenticationException"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_Succeeds() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + } catch (Exception e) { + Assert.fail("Should not have thrown " + e.getCause().getClass().getName() + ": " + e.getCause().getMessage()); + } + } + + @Test + public void GovernmentChannelValidation_NoAuthentication_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(null, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoAudienceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, ""); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_WrongAudienceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, "abc"); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_WrongAudienceClaimIssuer_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + ClaimsIdentity identity = new ClaimsIdentity("https://wrongissuer.com", claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoServiceClaim_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_NoServiceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, ""); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + @Test + public void GovernmentChannelValidation_WrongServiceClaimValue_Fails() { + String appId = "1234567890"; + String serviceUrl = "https://webchat.botframework.com/"; + CredentialProvider credentials = new SimpleCredentialProvider(appId, ""); + + Map claims = new HashMap(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, appId); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, "other"); + ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims); + + try { + GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join(); + Assert.fail("Should have thrown an Authorization exception"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof AuthenticationException); + } + } + + private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(String appId, String pwd, String channelService) throws IOException, ExecutionException, InterruptedException { + String header = "Bearer " + new MicrosoftAppCredentials(appId, pwd).getToken().join(); + JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(header, appId, pwd, channelService); + } + + private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(String header, String appId, String pwd, String channelService) { + CredentialProvider credentials = new SimpleCredentialProvider(appId, pwd); + ChannelProvider channel = new SimpleChannelProvider(channelService); + + ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, channel, null, "https://webchat.botframework.com/").join(); + + Assert.assertTrue(identity.isAuthenticated()); + } + + private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Throws(String header, String appId, String pwd, String channelService) throws ExecutionException, InterruptedException { + CredentialProvider credentials = new SimpleCredentialProvider(appId, pwd); + ChannelProvider channel = new SimpleChannelProvider(channelService); + + try { + JwtTokenValidation.validateAuthHeader( + header, + credentials, + channel, + "", + "https://webchat.botframework.com/").join(); + Assert.fail("Should have thrown AuthenticationException"); + } catch (AuthenticationException e) { + Assert.assertTrue(true); + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/MicrosoftAppCredentialsTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/MicrosoftAppCredentialsTests.java new file mode 100644 index 000000000..73d9af2ee --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/MicrosoftAppCredentialsTests.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.LocalDateTime; + +public class MicrosoftAppCredentialsTests { + + @Test + public void ValidateAuthEndpoint() { + try { + // In Java, about the only thing that can cause a MalformedURLException in a missing or unknown protocol. + // At any rate, this should validate someone didn't mess up the oAuth Endpoint for the class. + MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F"); + new URL(credentials.oAuthEndpoint()); + + credentials.setChannelAuthTenant("tenant.com"); + + MicrosoftAppCredentials credentialsWithTenant = + new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F", "tenant.com"); + + } catch(MalformedURLException e) { + Assert.fail("Should not have thrown MalformedURLException"); + } + } + +// @Test +// public void GetToken() { +// MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F"); +// String token = credentials.getToken().join(); +// Assert.assertFalse(StringUtils.isEmpty(token)); +// } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java deleted file mode 100644 index 0c9d229c6..000000000 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.microsoft.bot.connector; - -import com.microsoft.aad.adal4j.ClientCredential; -import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.connector.authentication.OAuthClient; -import com.microsoft.bot.connector.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.TokenResponse; -import com.microsoft.rest.RestClient; -import org.apache.commons.lang3.StringUtils; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - - -import static java.util.concurrent.CompletableFuture.completedFuture; - - -public class OAuthConnectorTest extends OAuthTestBase { - - - private ConnectorClientImpl mockConnectorClient; - private MicrosoftAppCredentials credentials; - - public OAuthConnectorTest() throws IOException, ExecutionException, InterruptedException, URISyntaxException { - super(RunCondition.BOTH); - - this.credentials = new MicrosoftAppCredentials(clientId, clientSecret); - } - - @Test(expected = IllegalArgumentException.class) - public void OAuthClient_ShouldThrowOnInvalidUrl() throws MalformedURLException, URISyntaxException { - - OAuthClient test = new OAuthClient(this.connector, "http://localhost"); - Assert.assertTrue( "Exception not thrown", false); - } - - - @Test(expected = IllegalArgumentException.class) - public void GetUserToken_ShouldThrowOnEmptyUserId() throws URISyntaxException, IOException, ExecutionException, InterruptedException { - OAuthClient client = new OAuthClient(this.connector, "https://localhost"); - client.GetUserTokenAsync("", "mockConnection", ""); - } - - @Test(expected = IllegalArgumentException.class) - public void GetUserToken_ShouldThrowOnEmptyConnectionName() throws URISyntaxException, IOException, ExecutionException, InterruptedException { - OAuthClient client = new OAuthClient(this.connector, "https://localhost"); - client.GetUserTokenAsync("userid", "", ""); - } -/* - TODO: Need to set up a bot and login with AADv2 to perform new recording (or convert the C# recordings) - @Test - public void GetUserToken_ShouldReturnTokenWithNoMagicCode() throws URISyntaxException, MalformedURLException { - - CompletableFuture authTest = this.UseOAuthClientFor((client) -> - { - TokenResponse token = null; - try { - System.out.println("This is a test asdfasdfasdf"); - token = await(client.GetUserTokenAsync("default-user", "mygithubconnection", "")); - if (null==token) { - System.out.println(String.format("This is a test 2 - NULL TOKEN")); - System.out.flush(); - } - else { - System.out.println(String.format("This is a test 2 - %s", token.token())); - System.out.flush(); - - } - - } catch (IOException e) { - e.printStackTrace(); - } catch (URISyntaxException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - Assert.assertNotNull(token); - Assert.assertFalse(StringUtils.isNotBlank(token.token())); - return completedFuture(null); - }, "OAuthConnectorTest", "GetUserToken_ShouldReturnTokenWithNoMagicCode"); - await(authTest); - } - - @Test -public async Task GetUserToken_ShouldReturnNullOnInvalidConnectionString() throws URISyntaxException { - await UseOAuthClientFor(async client => - { - var token = await client.GetUserTokenAsync("default-user", "mygithubconnection1", ""); - Assert.Null(token); - }); - } - - // @Test - Disabled due to bug in service - //public async Task GetSignInLinkAsync_ShouldReturnValidUrl() - //{ - // var activity = new Activity() - // { - // Id = "myid", - // From = new ChannelAccount() { Id = "fromId" }, - // ServiceUrl = "https://localhost" - // }; - // await UseOAuthClientFor(async client => - // { - // var uri = await client.GetSignInLinkAsync(activity, "mygithubconnection"); - // Assert.False(string.IsNullOrEmpty(uri)); - // Uri uriResult; - // Assert.True(Uri.TryCreate(uri, UriKind.Absolute, out uriResult) && uriResult.Scheme == Uri.UriSchemeHttps); - // }); - //} - - @Test -public async Task SignOutUser_ShouldThrowOnEmptyUserId() throws URISyntaxException { - var client = new OAuthClient(mockConnectorClient, "https://localhost"); - await Assert.ThrowsAsync(() => client.SignOutUserAsync("", "mockConnection")); - } - - @Test -public async Task SignOutUser_ShouldThrowOnEmptyConnectionName() throws URISyntaxException { - var client = new OAuthClient(mockConnectorClient, "https://localhost"); - await Assert.ThrowsAsync(() => client.SignOutUserAsync("userid", "")); - } - - @Test -public async Task GetSigninLink_ShouldThrowOnEmptyConnectionName() throws URISyntaxException { - var activity = new Activity(); - var client = new OAuthClient(mockConnectorClient, "https://localhost"); - await Assert.ThrowsAsync(() => client.GetSignInLinkAsync(activity, "")); - } - - @Test -public async Task GetSigninLink_ShouldThrowOnNullActivity() throws URISyntaxException { - var client = new OAuthClient(mockConnectorClient, "https://localhost"); - await Assert.ThrowsAsync(() => client.GetSignInLinkAsync(null, "mockConnection")); - } - */ - } - diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTests.java new file mode 100644 index 000000000..41351c98a --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthConnectorTests.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.connector.rest.RestOAuthClient; +import com.microsoft.bot.schema.AadResourceUrls; +import java.util.concurrent.CompletionException; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; + + +public class OAuthConnectorTests extends OAuthTestBase { + + + private RestConnectorClient mockConnectorClient; + private MicrosoftAppCredentials credentials; + + public OAuthConnectorTests() throws IOException, ExecutionException, InterruptedException, URISyntaxException { + super(RunCondition.BOTH); + + this.credentials = new MicrosoftAppCredentials(clientId, clientSecret); + } + + @Test + public void OAuthClient_ShouldNotThrowOnHttpUrl() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + } + + @Test(expected = NullPointerException.class) + public void OAuthClient_ShouldThrowOnNullCredentials() { + OAuthClient client = new RestOAuthClient("http://localhost", null); + } + + @Test(expected = CompletionException.class) + public void GetUserToken_ShouldThrowOnEmptyConnectionName() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getUserToken().getToken("userid", null).join(); + } + + @Test + public void GetUserToken_ShouldReturnNullOnInvalidConnectionstring() { + UseOAuthClientFor(client -> { + return client.getUserToken().getToken("default-user", "mygithubconnection1", null, null) + .thenApply(tokenResponse -> { + Assert.assertNull(tokenResponse); + return null; + }); + }).join(); + } + + @Test(expected = CompletionException.class) + public void SignOutUser_ShouldThrowOnEmptyUserId() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getUserToken().signOut(null).join(); + } + + @Test(expected = CompletionException.class) + public void GetSigninLink_ShouldThrowOnNullState() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getBotSignIn().getSignInUrl(null).join(); + } + + @Test(expected = CompletionException.class) + public void GetTokenStatus_ShouldThrowOnNullUserId() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getUserToken().getTokenStatus(null).join(); + } + + @Test(expected = CompletionException.class) + public void GetAadTokensAsync_ShouldThrowOnNullUserId() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getUserToken().getAadTokens(null, "connection", new AadResourceUrls()).join(); + } + + @Test(expected = CompletionException.class) + public void GetAadTokensAsync_ShouldThrowOnNullConncetionName() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getUserToken().getAadTokens("user", null, new AadResourceUrls()).join(); + } + + @Test(expected = CompletionException.class) + public void GetAadTokensAsync_ShouldThrowOnNullResourceUrls() { + OAuthClient client = new RestOAuthClient("http://localhost", new BotAccessTokenStub("token")); + client.getUserToken().getAadTokens("user", "connection", null).join(); + } +} + diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java index 3a9ed6b6b..ac572fd29 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/OAuthTestBase.java @@ -1,52 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; -import com.microsoft.bot.connector.authentication.AuthenticationConstants; import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.bot.connector.authentication.OAuthClient; import com.microsoft.bot.connector.base.TestBase; -import com.microsoft.bot.connector.implementation.ConnectorClientImpl; -import com.microsoft.bot.schema.models.ChannelAccount; -import com.microsoft.rest.RestClient; -import com.sun.jndi.toolkit.url.Uri; -import okhttp3.Request; -import org.apache.commons.io.FileSystemUtils; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.connector.rest.RestOAuthClient; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.restclient.RestClient; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; -import static java.util.concurrent.CompletableFuture.completedFuture; - -public class OAuthTestBase extends TestBase -{ +public class OAuthTestBase extends TestBase { protected String clientId; - protected String clientSecret ; + protected String clientSecret; protected final String userId = "U19KH8EHJ:T03CWQ0QB"; protected final String botId = "B21UTEF8S:T03CWQ0QB"; protected final static String hostUri = "https://slack.botframework.com"; private String token; - protected ConnectorClientImpl connector; + protected RestConnectorClient connector; + protected RestOAuthClient oAuthClient; private ChannelAccount bot; + public ChannelAccount getBot() { return this.bot; } private ChannelAccount user; + public ChannelAccount getUser() { return this.user; } - public OAuthTestBase() { + public OAuthTestBase() { super(RunCondition.BOTH); } @@ -71,60 +68,48 @@ protected void initializeClients(RestClient restClient, String botId, String use } } - this.connector = new ConnectorClientImpl(restClient); + this.connector = new RestConnectorClient(restClient); + this.oAuthClient = new RestOAuthClient(restClient); + if (this.clientId != null && this.clientSecret != null) { MicrosoftAppCredentials credentials = new MicrosoftAppCredentials(this.clientId, this.clientSecret); - - this.token = credentials.getToken(new Request.Builder().build()); - } - else { + this.token = credentials.getToken().join(); + } else { this.token = null; } - this.bot = new ChannelAccount() - .withId(botId); - this.user = new ChannelAccount() - .withId(userId); - - - + this.bot = new ChannelAccount(botId); + this.user = new ChannelAccount(userId); } @Override protected void cleanUpResources() { } + public void UseClientFor(Function> doTest) { this.UseClientFor(doTest, null, ""); } + public void UseClientFor(Function> doTest, String className) { this.UseClientFor(doTest, className, ""); } + public void UseClientFor(Function> doTest, String className, String methodName) { doTest.apply(this.connector).join(); } - public CompletableFuture UseOAuthClientFor(Function> doTest) throws MalformedURLException, URISyntaxException { + public CompletableFuture UseOAuthClientFor(Function> doTest) { return this.UseOAuthClientFor(doTest, null, ""); } - public CompletableFuture UseOAuthClientFor(Function> doTest, String className) throws MalformedURLException, URISyntaxException { + public CompletableFuture UseOAuthClientFor(Function> doTest, String className) { return this.UseOAuthClientFor(doTest, className, ""); } - public CompletableFuture UseOAuthClientFor(Function> doTest, String className, String methodName) throws MalformedURLException, URISyntaxException { - return CompletableFuture.runAsync(()->{ - OAuthClient oauthClient = null; - try { - oauthClient = new OAuthClient(this.connector, AuthenticationConstants.OAuthUrl); - } catch (URISyntaxException e) { - e.printStackTrace(); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - doTest.apply(oauthClient); - }); + public CompletableFuture UseOAuthClientFor(Function> doTest, String className, String methodName) { + return doTest.apply(oAuthClient); } } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryAfterHelperTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryAfterHelperTests.java new file mode 100644 index 000000000..155c96386 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryAfterHelperTests.java @@ -0,0 +1,95 @@ +package com.microsoft.bot.connector; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.MsalServiceException; +import com.microsoft.bot.connector.authentication.RetryAfterHelper; +import com.microsoft.bot.connector.authentication.RetryException; +import com.microsoft.bot.connector.authentication.RetryParams; + +import org.junit.Assert; +import org.junit.Test; + +public class RetryAfterHelperTests { + + @Test + public void TestRetryIncrement() { + RetryParams result = RetryAfterHelper.processRetry(new ArrayList(), 8); + Assert.assertTrue(result.getShouldRetry()); + result = RetryAfterHelper.processRetry(new ArrayList(), 9); + Assert.assertFalse(result.getShouldRetry()); + } + + @Test + public void TestRetryDelaySeconds() { + List headers = new ArrayList(); + headers.add("10"); + RetryParams result = RetryAfterHelper.processRetry(headers, 1); + Assert.assertEquals(result.getRetryAfter(), 10000); + } + + @Test + public void TestRetryDelayRFC1123Date() { + Instant instant = Instant.now().plusSeconds(5); + ZonedDateTime dateTime = instant.atZone(ZoneId.of("UTC")); + String dateTimeString = dateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME); + List headers = new ArrayList(); + headers.add(dateTimeString); + RetryParams result = RetryAfterHelper.processRetry(headers, 1); + Assert.assertTrue(result.getShouldRetry()); + Assert.assertTrue(result.getRetryAfter() > 0); + } + + @Test + public void TestRetryDelayRFC1123DateInPast() { + Instant instant = Instant.now().plusSeconds(-5); + ZonedDateTime dateTime = instant.atZone(ZoneId.of("UTC")); + String dateTimeString = dateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME); + List headers = new ArrayList(); + headers.add(dateTimeString); + RetryParams result = RetryAfterHelper.processRetry(headers, 1); + Assert.assertTrue(result.getShouldRetry()); + // default is 50, so since the time was in the past we should be seeing the default 50 here. + Assert.assertTrue(result.getRetryAfter() == 50); + } + + + @Test + public void TestRetryDelayRFC1123DateEmpty() { + List headers = new ArrayList(); + headers.add(""); + RetryParams result = RetryAfterHelper.processRetry(headers, 1); + Assert.assertTrue(result.getShouldRetry()); + // default is 50, so since the time was in the past we should be seeing the default 50 here. + Assert.assertTrue(result.getRetryAfter() == 50); + } + + @Test + public void TestRetryDelayRFC1123DateNull() { + List headers = new ArrayList(); + headers.add(null); + RetryParams result = RetryAfterHelper.processRetry(headers, 1); + Assert.assertTrue(result.getShouldRetry()); + // default is 50, so since the time was in the past we should be seeing the default 50 here. + Assert.assertTrue(result.getRetryAfter() == 50); + } + + @Test + public void TestRetryDelayRFC1123NeaderNull() { + RetryParams result = RetryAfterHelper.processRetry(null, 1); + Assert.assertTrue(result.getShouldRetry()); + // default is 50, so since the time was in the past we should be seeing the default 50 here. + Assert.assertTrue(result.getRetryAfter() == 50); + } + +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryParamsTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryParamsTests.java new file mode 100644 index 000000000..2757acbc9 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryParamsTests.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.RetryParams; +import org.junit.Assert; +import org.junit.Test; + +import java.time.Duration; + +public class RetryParamsTests { + @Test + public void RetryParams_StopRetryingValidation() { + RetryParams retryParams = RetryParams.stopRetrying(); + Assert.assertFalse(retryParams.getShouldRetry()); + } + + @Test + public void RetryParams_DefaultBackOffShouldRetryOnFirstRetry() { + RetryParams retryParams = RetryParams.defaultBackOff(0); + + Assert.assertTrue(retryParams.getShouldRetry()); + Assert.assertEquals(50, retryParams.getRetryAfter()); + } + + @Test + public void RetryParams_DefaultBackOffShouldNotRetryAfter5Retries() { + RetryParams retryParams = RetryParams.defaultBackOff(10); + Assert.assertFalse(retryParams.getShouldRetry()); + } + + @Test + public void RetryParams_DelayOutOfBounds() { + RetryParams retryParams = new RetryParams(Duration.ofSeconds(11).toMillis()); + Assert.assertEquals(Duration.ofSeconds(10).toMillis(), retryParams.getRetryAfter()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryTests.java new file mode 100644 index 000000000..f40de129b --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/RetryTests.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.Retry; +import com.microsoft.bot.connector.authentication.RetryException; +import com.microsoft.bot.connector.authentication.RetryParams; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class RetryTests { + @Test + public void Retry_NoRetryWhenTaskSucceeds() { + FaultyClass faultyClass = new FaultyClass(); + faultyClass.exceptionToThrow = null; + + Retry.run(() -> + faultyClass.faultyTask(), + ((e, integer) -> faultyClass.exceptionHandler(e, integer))) + .join(); + + Assert.assertNull(faultyClass.exceptionReceived); + Assert.assertEquals(1, faultyClass.callCount); + } + + @Test + public void Retry_RetryThenSucceed() { + FaultyClass faultyClass = new FaultyClass(); + faultyClass.exceptionToThrow = new IllegalArgumentException(); + faultyClass.triesUntilSuccess = 3; + + Retry.run(() -> + faultyClass.faultyTask(), + ((e, integer) -> faultyClass.exceptionHandler(e, integer))) + .join(); + + Assert.assertNotNull(faultyClass.exceptionReceived); + Assert.assertEquals(3, faultyClass.callCount); + } + + @Test + public void Retry_RetryUntilFailure() { + FaultyClass faultyClass = new FaultyClass(); + faultyClass.exceptionToThrow = new IllegalArgumentException(); + faultyClass.triesUntilSuccess = 12; + + try { + Retry.run(() -> + faultyClass.faultyTask(), + ((e, integer) -> faultyClass.exceptionHandler(e, integer))) + .join(); + Assert.fail("Should have thrown a RetryException because it exceeded max retry"); + } catch (CompletionException e) { + Assert.assertTrue(e.getCause() instanceof RetryException); + Assert.assertEquals(RetryParams.MAX_RETRIES, faultyClass.callCount); + Assert.assertTrue(RetryParams.MAX_RETRIES == ((RetryException) e.getCause()).getExceptions().size()); + } + } + + private static class FaultyClass { + RuntimeException exceptionToThrow; + RuntimeException exceptionReceived; + int latestRetryCount = 0; + int callCount = 0; + int triesUntilSuccess = 0; + + CompletableFuture faultyTask() { + callCount++; + + if (callCount < triesUntilSuccess && exceptionToThrow != null) { + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(exceptionToThrow); + return result; + } + + return CompletableFuture.completedFuture(null); + } + + RetryParams exceptionHandler(RuntimeException e, int currentRetryCount) { + exceptionReceived = e; + latestRetryCount = currentRetryCount; + return RetryParams.defaultBackOff(currentRetryCount); + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleChannelProviderTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleChannelProviderTests.java new file mode 100644 index 000000000..6ae6f4e72 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleChannelProviderTests.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.SimpleChannelProvider; +import org.junit.Assert; +import org.junit.Test; + +public class SimpleChannelProviderTests { + @Test + public void PublicChannelProvider() { + SimpleChannelProvider channel = new SimpleChannelProvider(); + Assert.assertTrue(channel.isPublicAzure()); + Assert.assertFalse(channel.isGovernment()); + } + + @Test + public void GovernmentChannelProvider() { + SimpleChannelProvider channel = new SimpleChannelProvider(GovernmentAuthenticationConstants.CHANNELSERVICE); + Assert.assertFalse(channel.isPublicAzure()); + Assert.assertTrue(channel.isGovernment()); + } + + @Test + public void GetChannelService() { + try { + SimpleChannelProvider channel = new SimpleChannelProvider(GovernmentAuthenticationConstants.CHANNELSERVICE); + String service = channel.getChannelService().join(); + Assert.assertEquals(service, GovernmentAuthenticationConstants.CHANNELSERVICE); + } catch (Throwable t) { + Assert.fail("Should not have thrown " + t.getClass().getName()); + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleCredentialProviderTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleCredentialProviderTests.java new file mode 100644 index 000000000..7f04f3387 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SimpleCredentialProviderTests.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; +import org.junit.Assert; +import org.junit.Test; + +public class SimpleCredentialProviderTests { + @Test + public void ValidAppId() { + SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider("appid", "pwd"); + + Assert.assertTrue(credentialProvider.isValidAppId("appid").join()); + Assert.assertFalse(credentialProvider.isValidAppId("wrongappid").join()); + } + + @Test + public void AppPassword() { + SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider("appid", "pwd"); + + Assert.assertEquals(credentialProvider.getAppPassword("appid").join(), "pwd"); + Assert.assertNull(credentialProvider.getAppPassword("wrongappid").join()); + } + + @Test + public void AuthenticationDisabled() { + Assert.assertFalse(new SimpleCredentialProvider("appid", "pwd").isAuthenticationDisabled().join()); + Assert.assertTrue(new SimpleCredentialProvider(null, null).isAuthenticationDisabled().join()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SkillValidationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SkillValidationTests.java new file mode 100644 index 000000000..a04d4dad3 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/SkillValidationTests.java @@ -0,0 +1,139 @@ +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.CredentialProvider; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.connector.authentication.SkillValidation; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class SkillValidationTests { + @Test + public void IsSkillClaimTest() { + Map claims = new HashMap<>(); + String audience = UUID.randomUUID().toString(); + String appId = UUID.randomUUID().toString(); + + // Empty list of claims + Assert.assertFalse(SkillValidation.isSkillClaim(claims)); + + // No Audience claim + claims.put(AuthenticationConstants.VERSION_CLAIM, "1.0"); + Assert.assertFalse(SkillValidation.isSkillClaim(claims)); + + // Emulator Audience claim + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + Assert.assertFalse(SkillValidation.isSkillClaim(claims)); + + // No AppId claim + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, audience); + Assert.assertFalse(SkillValidation.isSkillClaim(claims)); + + // AppId != Audience + claims.put(AuthenticationConstants.APPID_CLAIM, audience); + Assert.assertFalse(SkillValidation.isSkillClaim(claims)); + + // Anonymous skill app id + claims.put(AuthenticationConstants.APPID_CLAIM, AuthenticationConstants.ANONYMOUS_SKILL_APPID); + Assert.assertTrue(SkillValidation.isSkillClaim(claims)); + + // All checks pass, should be good now + claims.put(AuthenticationConstants.APPID_CLAIM, appId); + Assert.assertTrue(SkillValidation.isSkillClaim(claims)); + } + + @Test + public void IsSkillTokenTest() { + Assert.assertEquals("Null String", false, SkillValidation.isSkillToken(null)); + Assert.assertEquals("Empty String", false, SkillValidation.isSkillToken("")); + Assert.assertEquals("No token Part", false, SkillValidation.isSkillToken("Bearer")); + Assert.assertEquals("No bearer part", false, SkillValidation.isSkillToken("ew0KICAiYWxnIjogIlJTMjU2IiwNCiAgImtpZCI6ICJKVzNFWGRudy13WTJFcUxyV1RxUTJyVWtCLWciLA0KICAieDV0IjogIkpXM0VYZG53LXdZMkVxTHJXVHFRMnJVa0ItZyIsDQogICJ0eXAiOiAiSldUIg0KfQ.ew0KICAic2VydmljZXVybCI6ICJodHRwczovL2RpcmVjdGxpbmUuYm90ZnJhbWV3b3JrLmNvbS8iLA0KICAibmJmIjogMTU3MTE5MDM0OCwNCiAgImV4cCI6IDE1NzExOTA5NDgsDQogICJpc3MiOiAiaHR0cHM6Ly9hcGkuYm90ZnJhbWV3b3JrLmNvbSIsDQogICJhdWQiOiAiNGMwMDM5ZTUtNjgxNi00OGU4LWIzMTMtZjc3NjkxZmYxYzVlIg0KfQ.cEVHmQCTjL9HVHGk91sja5CqjgvM7B-nArkOg4bE83m762S_le94--GBb0_7aAy6DCdvkZP0d4yWwbpfOkukEXixCDZQM2kWPcOo6lz_VIuXxHFlZAGrTvJ1QkBsg7vk-6_HR8XSLJQZoWrVhE-E_dPj4GPBKE6s1aNxYytzazbKRAEYa8Cn4iVtuYbuj4XfH8PMDv5aC0APNvfgTGk-BlIiP6AGdo4JYs62lUZVSAYg5VLdBcJYMYcKt-h2n1saeapFDVHx_tdpRuke42M4RpGH_wzICeWC5tTExWEkQWApU85HRA5zzk4OpTv17Ct13JCvQ7cD5x9RK5f7CMnbhQ")); + Assert.assertEquals("Invalid scheme", false, SkillValidation.isSkillToken("Potato ew0KICAiYWxnIjogIlJTMjU2IiwNCiAgImtpZCI6ICJKVzNFWGRudy13WTJFcUxyV1RxUTJyVWtCLWciLA0KICAieDV0IjogIkpXM0VYZG53LXdZMkVxTHJXVHFRMnJVa0ItZyIsDQogICJ0eXAiOiAiSldUIg0KfQ.ew0KICAic2VydmljZXVybCI6ICJodHRwczovL2RpcmVjdGxpbmUuYm90ZnJhbWV3b3JrLmNvbS8iLA0KICAibmJmIjogMTU3MTE5MDM0OCwNCiAgImV4cCI6IDE1NzExOTA5NDgsDQogICJpc3MiOiAiaHR0cHM6Ly9hcGkuYm90ZnJhbWV3b3JrLmNvbSIsDQogICJhdWQiOiAiNGMwMDM5ZTUtNjgxNi00OGU4LWIzMTMtZjc3NjkxZmYxYzVlIg0KfQ.cEVHmQCTjL9HVHGk91sja5CqjgvM7B-nArkOg4bE83m762S_le94--GBb0_7aAy6DCdvkZP0d4yWwbpfOkukEXixCDZQM2kWPcOo6lz_VIuXxHFlZAGrTvJ1QkBsg7vk-6_HR8XSLJQZoWrVhE-E_dPj4GPBKE6s1aNxYytzazbKRAEYa8Cn4iVtuYbuj4XfH8PMDv5aC0APNvfgTGk-BlIiP6AGdo4JYs62lUZVSAYg5VLdBcJYMYcKt-h2n1saeapFDVHx_tdpRuke42M4RpGH_wzICeWC5tTExWEkQWApU85HRA5zzk4OpTv17Ct13JCvQ7cD5x9RK5f7CMnbhQ")); + Assert.assertEquals("To bot v2 from webchat", false, SkillValidation.isSkillToken("Bearer ew0KICAiYWxnIjogIlJTMjU2IiwNCiAgImtpZCI6ICJKVzNFWGRudy13WTJFcUxyV1RxUTJyVWtCLWciLA0KICAieDV0IjogIkpXM0VYZG53LXdZMkVxTHJXVHFRMnJVa0ItZyIsDQogICJ0eXAiOiAiSldUIg0KfQ.ew0KICAic2VydmljZXVybCI6ICJodHRwczovL2RpcmVjdGxpbmUuYm90ZnJhbWV3b3JrLmNvbS8iLA0KICAibmJmIjogMTU3MTE5MDM0OCwNCiAgImV4cCI6IDE1NzExOTA5NDgsDQogICJpc3MiOiAiaHR0cHM6Ly9hcGkuYm90ZnJhbWV3b3JrLmNvbSIsDQogICJhdWQiOiAiNGMwMDM5ZTUtNjgxNi00OGU4LWIzMTMtZjc3NjkxZmYxYzVlIg0KfQ.cEVHmQCTjL9HVHGk91sja5CqjgvM7B-nArkOg4bE83m762S_le94--GBb0_7aAy6DCdvkZP0d4yWwbpfOkukEXixCDZQM2kWPcOo6lz_VIuXxHFlZAGrTvJ1QkBsg7vk-6_HR8XSLJQZoWrVhE-E_dPj4GPBKE6s1aNxYytzazbKRAEYa8Cn4iVtuYbuj4XfH8PMDv5aC0APNvfgTGk-BlIiP6AGdo4JYs62lUZVSAYg5VLdBcJYMYcKt-h2n1saeapFDVHx_tdpRuke42M4RpGH_wzICeWC5tTExWEkQWApU85HRA5zzk4OpTv17Ct13JCvQ7cD5x9RK5f7CMnbhQ")); + Assert.assertEquals("To bot v1 token from emulator", false, SkillValidation.isSkillToken("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzMzYzQyMS1mN2QzLTRiNmMtOTkyYi0zNmU3ZTZkZTg3NjEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTcxMTg5ODczLCJuYmYiOjE1NzExODk4NzMsImV4cCI6MTU3MTE5Mzc3MywiYWlvIjoiNDJWZ1lLaWJGUDIyMUxmL0NjL1Yzai8zcGF2RUFBPT0iLCJhcHBpZCI6IjRjMzNjNDIxLWY3ZDMtNGI2Yy05OTJiLTM2ZTdlNmRlODc2MSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJOdXJ3bTVOQnkwR2duT3dKRnFVREFBIiwidmVyIjoiMS4wIn0.GcKs3XZ_4GONVsAoPYI7otqUZPoNN8pULUnlJMxQa-JKXRKV0KtvTAdcMsfYudYxbz7HwcNYerFT1q3RZAimJFtfF4x_sMN23yEVxsQmYQrsf2YPmEsbCfNiEx0YEoWUdS38R1N0Iul2P_P_ZB7XreG4aR5dT6lY5TlXbhputv9pi_yAU7PB1aLuB05phQme5NwJEY22pUfx5pe1wVHogI0JyNLi-6gdoSL63DJ32tbQjr2DNYilPVtLsUkkz7fTky5OKd4p7FmG7P5EbEK4H5j04AGe_nIFs-X6x_FIS_5OSGK4LGA2RPnqa-JYpngzlNWVkUbnuH10AovcAprgdg")); + Assert.assertEquals("To bot v2 token from emulator", false, SkillValidation.isSkillToken("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzAwMzllNS02ODE2LTQ4ZTgtYjMxMy1mNzc2OTFmZjFjNWUiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiL3YyLjAiLCJpYXQiOjE1NzExODkwMTEsIm5iZiI6MTU3MTE4OTAxMSwiZXhwIjoxNTcxMTkyOTExLCJhaW8iOiI0MlZnWUxnYWxmUE90Y2IxaEoxNzJvbmxIc3ZuQUFBPSIsImF6cCI6IjRjMDAzOWU1LTY4MTYtNDhlOC1iMzEzLWY3NzY5MWZmMWM1ZSIsImF6cGFjciI6IjEiLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJucEVxVTFoR1pVbXlISy1MUVdJQ0FBIiwidmVyIjoiMi4wIn0.CXcPx7LfatlRsOX4QG-jaC-guwcY3PFxpFICqwfoOTxAjHpeJNFXOpFeA3Qb5VKM6Yw5LyA9eraL5QDJB_4uMLCCKErPXMyoSm8Hw-GGZkHgFV5ciQXSXhE-IfOinqHE_0Lkt_VLR2q6ekOncnJeCR111QCqt3D8R0Ud0gvyLv_oONxDtqg7HUgNGEfioB-BDnBsO4RN7NGrWQFbyPxPmhi8a_Xc7j5Bb9jeiiIQbVaWkIrrPN31aWY1tEZLvdN0VluYlOa0EBVrzpXXZkIyWx99mpklg0lsy7mRyjuM1xydmyyGkzbiCKtODOanf8UwTjkTg5XTIluxe79_hVk2JQ")); + Assert.assertEquals("To skill valid v1 token", true, SkillValidation.isSkillToken("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzMzYzQyMS1mN2QzLTRiNmMtOTkyYi0zNmU3ZTZkZTg3NjEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTcxMTg5NjMwLCJuYmYiOjE1NzExODk2MzAsImV4cCI6MTU3MTE5MzUzMCwiYWlvIjoiNDJWZ1lJZzY1aDFXTUVPd2JmTXIwNjM5V1lLckFBPT0iLCJhcHBpZCI6IjRjMDAzOWU1LTY4MTYtNDhlOC1iMzEzLWY3NzY5MWZmMWM1ZSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJhWlpOUTY3RjRVNnNmY3d0S0R3RUFBIiwidmVyIjoiMS4wIn0.Yogk9fptxxJKO8jRkk6FrlLQsAulNNgoa0Lqv2JPkswyyizse8kcwQhxOaZOotY0UBduJ-pCcrejk6k4_O_ZReYXKz8biL9Q7Z02cU9WUMvuIGpAhttz8v0VlVSyaEJVJALc5B-U6XVUpZtG9LpE6MVror_0WMnT6T9Ijf9SuxUvdVCcmAJyZuoqudodseuFI-jtCpImEapZp0wVN4BUodrBacMbTeYjdZyAbNVBqF5gyzDztMKZR26HEz91gqulYZvJJZOJO6ejnm0j62s1tqvUVRBywvnSOon-MV0Xt2Vm0irhv6ipzTXKwWhT9rGHSLj0g8r6NqWRyPRFqLccvA")); + Assert.assertEquals("To skill valid v2 token", true, SkillValidation.isSkillToken("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzAwMzllNS02ODE2LTQ4ZTgtYjMxMy1mNzc2OTFmZjFjNWUiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiL3YyLjAiLCJpYXQiOjE1NzExODk3NTUsIm5iZiI6MTU3MTE4OTc1NSwiZXhwIjoxNTcxMTkzNjU1LCJhaW8iOiI0MlZnWUpnZDROZkZKeG1tMTdPaVMvUk8wZll2QUE9PSIsImF6cCI6IjRjMzNjNDIxLWY3ZDMtNGI2Yy05OTJiLTM2ZTdlNmRlODc2MSIsImF6cGFjciI6IjEiLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJMc2ZQME9JVkNVS1JzZ1IyYlFBQkFBIiwidmVyIjoiMi4wIn0.SggsEbEyXDYcg6EdhK-RA1y6S97z4hwEccXc6a3ymnHP-78frZ3N8rPLsqLoK5QPGA_cqOXsX1zduA4vlFSy3MfTV_npPfsyWa1FIse96-2_3qa9DIP8bhvOHXEVZeq-r-0iF972waFyPPC_KVYWnIgAcunGhFWvLhhOUx9dPgq7824qTq45ma1rOqRoYbhhlRn6PJDymIin5LeOzDGJJ8YVLnFUgntc6_4z0P_fnuMktzar88CUTtGvR4P7XNJhS8v9EwYQujglsJNXg7LNcwV7qOxDYWJtT_UMuMAts9ctD6FkuTGX_-6FTqmdUPPUS4RWwm4kkl96F_dXnos9JA")); + } + + @Test + public void IdentityValidationTests() { + String audience = UUID.randomUUID().toString(); + String appId = UUID.randomUUID().toString(); + Map claims = new HashMap<>(); + ClaimsIdentity mockIdentity = mock(ClaimsIdentity.class); + CredentialProvider mockCredentials = mock(CredentialProvider.class); + + // Null identity + CompletionException exception = Assert.assertThrows(CompletionException.class, () -> { + SkillValidation.validateIdentity(null, null).join(); + }); + Assert.assertEquals("Invalid Identity", exception.getCause().getMessage()); + + // not authenticated identity + when(mockIdentity.isAuthenticated()).thenReturn(false); + exception = Assert.assertThrows(CompletionException.class, () -> { + SkillValidation.validateIdentity(mockIdentity, null).join(); + }); + Assert.assertEquals("Token Not Authenticated", exception.getCause().getMessage()); + + // No version claims + when(mockIdentity.isAuthenticated()).thenReturn(true); + when(mockIdentity.claims()).thenReturn(claims); + exception = Assert.assertThrows(CompletionException.class, () -> { + SkillValidation.validateIdentity(mockIdentity, null).join(); + }); + Assert.assertEquals( + AuthenticationConstants.VERSION_CLAIM + " claim is required on skill Tokens.", + exception.getCause().getMessage() + ); + + // No audience claim + claims.put(AuthenticationConstants.VERSION_CLAIM, "1.0"); + exception = Assert.assertThrows(CompletionException.class, () -> { + SkillValidation.validateIdentity(mockIdentity, null).join(); + }); + Assert.assertEquals( + AuthenticationConstants.AUDIENCE_CLAIM + " claim is required on skill Tokens.", + exception.getCause().getMessage() + ); + + // Invalid AppId in in appId or azp + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, audience); + when(mockCredentials.isValidAppId(any())).thenReturn(CompletableFuture.completedFuture(true)); + exception = Assert.assertThrows(CompletionException.class, () -> { + SkillValidation.validateIdentity(mockIdentity, mockCredentials).join(); + }); + Assert.assertEquals("Invalid appId.", exception.getCause().getMessage()); + + // Invalid AppId in audience + claims.put(AuthenticationConstants.APPID_CLAIM, appId); + when(mockCredentials.isValidAppId(any())).thenReturn(CompletableFuture.completedFuture(false)); + exception = Assert.assertThrows(CompletionException.class, () -> { + SkillValidation.validateIdentity(mockIdentity, mockCredentials).join(); + }); + Assert.assertEquals("Invalid audience.", exception.getCause().getMessage()); + + // All checks pass (no exception thrown) + when(mockCredentials.isValidAppId(any())).thenReturn(CompletableFuture.completedFuture(true)); + SkillValidation.validateIdentity(mockIdentity, mockCredentials).join(); + } + + @Test + public void CreateAnonymousSkillClaimTest() { + ClaimsIdentity sut = SkillValidation.createAnonymousSkillClaim(); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_SKILL_APPID, JwtTokenValidation.getAppIdFromClaims(sut.claims())); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, sut.getType()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/UserAgentTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/UserAgentTest.java index 53a40783a..b125a6169 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/UserAgentTest.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/UserAgentTest.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector; import org.junit.Assert; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/InterceptorManager.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/InterceptorManager.java index 8488a2b00..4d24e0b2d 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/InterceptorManager.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/InterceptorManager.java @@ -1,10 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.base; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.common.io.BaseEncoding; import okhttp3.*; -import okhttp3.internal.Util; import okio.Buffer; import okio.BufferedSource; @@ -18,6 +20,8 @@ import java.util.Map; import java.util.zip.GZIPInputStream; +import static java.nio.charset.StandardCharsets.UTF_8; + public class InterceptorManager { private final static String RECORD_FOLDER = "session-records/"; @@ -252,7 +256,7 @@ private void extractResponseData(Map responseData, Response resp if (contentType != null) { if (contentType.startsWith("application/json")) { - content = buffer.readString(Util.UTF_8); + content = buffer.readString(UTF_8); } else { content = BaseEncoding.base64().encode(buffer.readByteArray()); } diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/NetworkCallRecord.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/NetworkCallRecord.java index 2341e6f87..53c1117c7 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/NetworkCallRecord.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/NetworkCallRecord.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.base; import java.util.Map; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/RecordedData.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/RecordedData.java index bbd40fdef..0d73b7b03 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/RecordedData.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/RecordedData.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.base; import java.util.LinkedList; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/TestBase.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/TestBase.java index c8c91478d..a673ec18c 100644 --- a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/TestBase.java +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/base/TestBase.java @@ -1,13 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.bot.connector.base; import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; -import com.microsoft.rest.LogLevel; -import com.microsoft.rest.RestClient; -import com.microsoft.rest.ServiceResponseBuilder; -import com.microsoft.rest.credentials.ServiceClientCredentials; -import com.microsoft.rest.credentials.TokenCredentials; -import com.microsoft.rest.interceptors.LoggingInterceptor; -import com.microsoft.rest.serializer.JacksonAdapter; +import com.microsoft.bot.restclient.LogLevel; +import com.microsoft.bot.restclient.RestClient; +import com.microsoft.bot.restclient.ServiceResponseBuilder; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.credentials.TokenCredentials; +import com.microsoft.bot.restclient.interceptors.LoggingInterceptor; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; import org.junit.*; import org.junit.rules.TestName; diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AdditionalPropertiesSerializerTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AdditionalPropertiesSerializerTests.java new file mode 100644 index 000000000..1f2ff7da6 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AdditionalPropertiesSerializerTests.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.restclient.util.Foo; +import com.microsoft.bot.restclient.util.FooChild; +import org.junit.Assert; +import org.junit.Test; +import org.json.JSONException; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.util.ArrayList; +import java.util.HashMap; + +public class AdditionalPropertiesSerializerTests { + public void assertJsonEqualsNonStrict(String json1, String json2) { + try { + JSONAssert.assertEquals(json1, json2, false); + } catch (JSONException jse) { + throw new IllegalArgumentException(jse.getMessage()); + } + } + + @Test + public void canSerializeAdditionalProperties() throws Exception { + Foo foo = new Foo(); + foo.bar = "hello.world"; + foo.baz = new ArrayList<>(); + foo.baz.add("hello"); + foo.baz.add("hello.world"); + foo.qux = new HashMap<>(); + foo.qux.put("hello", "world"); + foo.qux.put("a.b", "c.d"); + foo.qux.put("bar.a", "ttyy"); + foo.qux.put("bar.b", "uuzz"); + foo.additionalProperties = new HashMap<>(); + foo.additionalProperties.put("bar", "baz"); + foo.additionalProperties.put("a.b", "c.d"); + foo.additionalProperties.put("properties.bar", "barbar"); + + String serialized = new JacksonAdapter().serialize(foo); + String expected = "{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + assertJsonEqualsNonStrict(expected, serialized); + } + + @Test + public void canDeserializeAdditionalProperties() throws Exception { + String wireValue = "{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + Foo deserialized = new JacksonAdapter().deserialize(wireValue, Foo.class); + Assert.assertNotNull(deserialized.additionalProperties); + Assert.assertEquals("baz", deserialized.additionalProperties.get("bar")); + Assert.assertEquals("c.d", deserialized.additionalProperties.get("a.b")); + Assert.assertEquals("barbar", deserialized.additionalProperties.get("properties.bar")); + } + + @Test + public void canSerializeAdditionalPropertiesThroughInheritance() throws Exception { + Foo foo = new FooChild(); + foo.bar = "hello.world"; + foo.baz = new ArrayList<>(); + foo.baz.add("hello"); + foo.baz.add("hello.world"); + foo.qux = new HashMap<>(); + foo.qux.put("hello", "world"); + foo.qux.put("a.b", "c.d"); + foo.qux.put("bar.a", "ttyy"); + foo.qux.put("bar.b", "uuzz"); + foo.additionalProperties = new HashMap<>(); + foo.additionalProperties.put("bar", "baz"); + foo.additionalProperties.put("a.b", "c.d"); + foo.additionalProperties.put("properties.bar", "barbar"); + + String serialized = new JacksonAdapter().serialize(foo); + String expected = "{\"$type\":\"foochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + assertJsonEqualsNonStrict(expected, serialized); + } + + @Test + public void canDeserializeAdditionalPropertiesThroughInheritance() throws Exception { + String wireValue = "{\"$type\":\"foochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + Foo deserialized = new JacksonAdapter().deserialize(wireValue, Foo.class); + Assert.assertNotNull(deserialized.additionalProperties); + Assert.assertEquals("baz", deserialized.additionalProperties.get("bar")); + Assert.assertEquals("c.d", deserialized.additionalProperties.get("a.b")); + Assert.assertEquals("barbar", deserialized.additionalProperties.get("properties.bar")); + Assert.assertTrue(deserialized instanceof FooChild); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AnimalShelter.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AnimalShelter.java new file mode 100644 index 000000000..2fa5fe22f --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AnimalShelter.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.restclient.serializer.JsonFlatten; + +import java.util.List; + +@JsonFlatten +public class AnimalShelter { + + @JsonProperty(value = "properties.description") + private String description; + + @JsonProperty(value = "properties.animalsInfo", required = true) + private List animalsInfo; + + public String description() { + return this.description; + } + + public AnimalShelter withDescription(String description) { + this.description = description; + return this; + } + + public List animalsInfo() { + return this.animalsInfo; + } + + public AnimalShelter withAnimalsInfo(List animalsInfo) { + this.animalsInfo = animalsInfo; + return this; + } + +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AnimalWithTypeIdContainingDot.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AnimalWithTypeIdContainingDot.java new file mode 100644 index 000000000..fb472db7f --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/AnimalWithTypeIdContainingDot.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = AnimalWithTypeIdContainingDot.class) +@JsonTypeName("AnimalWithTypeIdContainingDot") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "#Favourite.Pet.DogWithTypeIdContainingDot", value = DogWithTypeIdContainingDot.class), + @JsonSubTypes.Type(name = "#Favourite.Pet.CatWithTypeIdContainingDot", value = CatWithTypeIdContainingDot.class), + @JsonSubTypes.Type(name = "#Favourite.Pet.RabbitWithTypeIdContainingDot", value = RabbitWithTypeIdContainingDot.class) +}) +public class AnimalWithTypeIdContainingDot { +} + diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/CatWithTypeIdContainingDot.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/CatWithTypeIdContainingDot.java new file mode 100644 index 000000000..6000619bb --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/CatWithTypeIdContainingDot.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = CatWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.CatWithTypeIdContainingDot") +public class CatWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { + @JsonProperty(value = "breed", required = true) + private String breed; + + public String breed() { + return this.breed; + } + + public CatWithTypeIdContainingDot withBreed(String presetName) { + this.breed = presetName; + return this; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ComposeTurtles.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ComposeTurtles.java new file mode 100644 index 000000000..9227fb385 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ComposeTurtles.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class ComposeTurtles { + @JsonProperty(value = "description") + private String description; + + @JsonProperty(value = "turtlesSet1Lead") + private TurtleWithTypeIdContainingDot turtlesSet1Lead; + + @JsonProperty(value = "turtlesSet1") + private List turtlesSet1; + + @JsonProperty(value = "turtlesSet2Lead") + private NonEmptyAnimalWithTypeIdContainingDot turtlesSet2Lead; + + @JsonProperty(value = "turtlesSet2") + private List turtlesSet2; + + public String description() { + return this.description; + } + + public ComposeTurtles withDescription(String description) { + this.description = description; + return this; + } + + public List turtlesSet1() { + return this.turtlesSet1; + } + + public TurtleWithTypeIdContainingDot turtlesSet1Lead() { + return this.turtlesSet1Lead; + } + + public ComposeTurtles withTurtlesSet1Lead(TurtleWithTypeIdContainingDot lead) { + this.turtlesSet1Lead = lead; + return this; + } + + public ComposeTurtles withTurtlesSet1(List turtles) { + this.turtlesSet1 = turtles; + return this; + } + + public List turtlesSet2() { + return this.turtlesSet2; + } + + public NonEmptyAnimalWithTypeIdContainingDot turtlesSet2Lead() { + return this.turtlesSet2Lead; + } + + public ComposeTurtles withTurtlesSet2Lead(NonEmptyAnimalWithTypeIdContainingDot lead) { + this.turtlesSet2Lead = lead; + return this; + } + + public ComposeTurtles withTurtlesSet2(List turtles) { + this.turtlesSet2 = turtles; + return this; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ConnectionPoolTests.java.dep b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ConnectionPoolTests.java.dep new file mode 100644 index 000000000..26e43a97b --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ConnectionPoolTests.java.dep @@ -0,0 +1,163 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.rest; + +import com.microsoft.rest.ServiceResponseBuilder.Factory; +import com.microsoft.rest.serializer.JacksonAdapter; +import okhttp3.Dispatcher; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.apache.commons.lang3.time.StopWatch; +import org.junit.Assert; +import org.junit.Test; +import retrofit2.http.GET; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; + +public class ConnectionPoolTests { + // Simulates a server with response latency of 1 second. A connection pool + // size 2 should only send 2 requests per second. + @Test + public void canUseOkHttpThreadPool() throws Exception { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("https://microsoft.com") + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new Factory()) + .withDispatcher(new Dispatcher(Executors.newFixedThreadPool(2))) + .useHttpClientThreadPool(true) + .withInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new IOException(e); + } + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }) + .build(); + + final Service service = restClient.retrofit().create(Service.class); + + final CountDownLatch latch = new CountDownLatch(1); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + Observable.range(1, 4) + .flatMap(new Func1>() { + @Override + public Observable call(Integer integer) { + return service.getAsync().subscribeOn(Schedulers.io()); + } + }) + .doOnCompleted(new Action0() { + @Override + public void call() { + latch.countDown(); + } + }).subscribe(); + + latch.await(); + stopWatch.stop(); + Assert.assertTrue(stopWatch.getTime() > 2000); + } + + // Simulates a server with response latency of 1 second. A connection pool + // size 2 should only send requests on Rx scheduler. + @Test + public void canUseRxThreadPool() throws Exception { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("https://microsoft.com") + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new Factory()) + .withDispatcher(new Dispatcher(Executors.newFixedThreadPool(2))) + .useHttpClientThreadPool(false) + .withInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new IOException(e); + } + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }) + .build(); + + final Service service = restClient.retrofit().create(Service.class); + + final CountDownLatch latch = new CountDownLatch(1); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + // Rx Scheduler with no concurrency control + Observable.range(1, 6) + .flatMap(new Func1>() { + @Override + public Observable call(Integer integer) { + return service.getAsync().subscribeOn(Schedulers.io()); + } + }) + .doOnCompleted(new Action0() { + @Override + public void call() { + latch.countDown(); + } + }).subscribe(); + + latch.await(); + stopWatch.stop(); + Assert.assertTrue(stopWatch.getTime() < 2000); + + final CountDownLatch latch2 = new CountDownLatch(1); + stopWatch.reset(); + stopWatch.start(); + + // Rx Scheduler with concurrency control + Observable.range(1, 4) + .flatMap(new Func1>() { + @Override + public Observable call(Integer integer) { + return service.getAsync().subscribeOn(Schedulers.io()); + } + }, 2) + .doOnCompleted(new Action0() { + @Override + public void call() { + latch2.countDown(); + } + }).subscribe(); + + latch2.await(); + stopWatch.stop(); + Assert.assertTrue(stopWatch.getTime() > 2000); + } + + private interface Service { + @GET("/") + Observable> getAsync(); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/CredentialsTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/CredentialsTests.java new file mode 100644 index 000000000..92ed4a4f4 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/CredentialsTests.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import com.microsoft.bot.restclient.credentials.BasicAuthenticationCredentials; +import com.microsoft.bot.restclient.credentials.TokenCredentials; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Assert; +import org.junit.Test; +import retrofit2.Retrofit; + +import java.io.IOException; + +public class CredentialsTests { + @Test + public void basicCredentialsTest() throws Exception { + BasicAuthenticationCredentials credentials = new BasicAuthenticationCredentials("user", "pass"); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + credentials.applyCredentialsFilter(clientBuilder); + clientBuilder.addInterceptor( + new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("Authorization"); + Assert.assertEquals("Basic dXNlcjpwYXNz", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + @Test + public void tokenCredentialsTest() throws Exception { + TokenCredentials credentials = new TokenCredentials(null, "this_is_a_token"); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + credentials.applyCredentialsFilter(clientBuilder); + clientBuilder.addInterceptor( + new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("Authorization"); + Assert.assertEquals("Bearer this_is_a_token", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/DogWithTypeIdContainingDot.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/DogWithTypeIdContainingDot.java new file mode 100644 index 000000000..674eaf18b --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/DogWithTypeIdContainingDot.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = DogWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.DogWithTypeIdContainingDot") +public class DogWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { + @JsonProperty(value = "breed") + private String breed; + + // Flattenable property + @JsonProperty(value = "properties.cuteLevel") + private Integer cuteLevel; + + public String breed() { + return this.breed; + } + + public DogWithTypeIdContainingDot withBreed(String audioLanguage) { + this.breed = audioLanguage; + return this; + } + + public Integer cuteLevel() { + return this.cuteLevel; + } + + public DogWithTypeIdContainingDot withCuteLevel(Integer cuteLevel) { + this.cuteLevel = cuteLevel; + return this; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/FlattenableAnimalInfo.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/FlattenableAnimalInfo.java new file mode 100644 index 000000000..14e4cd19e --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/FlattenableAnimalInfo.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FlattenableAnimalInfo { + + @JsonProperty(value = "home") + private String home; + + @JsonProperty(value = "animal", required = true) + private AnimalWithTypeIdContainingDot animal; + + public String home() { + return this.home; + } + + public FlattenableAnimalInfo withHome(String home) { + this.home = home; + return this; + } + + public AnimalWithTypeIdContainingDot animal() { + return this.animal; + } + + public FlattenableAnimalInfo withAnimal(AnimalWithTypeIdContainingDot animal) { + this.animal = animal; + return this; + } + +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/FlatteningSerializerTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/FlatteningSerializerTests.java new file mode 100644 index 000000000..14980b787 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/FlatteningSerializerTests.java @@ -0,0 +1,693 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.restclient.serializer.JsonFlatten; +import com.microsoft.bot.restclient.util.Foo; +import org.junit.Assert; +import org.junit.Test; +import org.json.JSONException; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FlatteningSerializerTests { + public void assertJsonEqualsNonStrict(String json1, String json2) { + try { + JSONAssert.assertEquals(json1, json2, false); + } catch (JSONException jse) { + throw new IllegalArgumentException(jse.getMessage()); + } + } + + @Test + public void canFlatten() throws Exception { + Foo foo = new Foo(); + foo.bar = "hello.world"; + foo.baz = new ArrayList<>(); + foo.baz.add("hello"); + foo.baz.add("hello.world"); + foo.qux = new HashMap<>(); + foo.qux.put("hello", "world"); + foo.qux.put("a.b", "c.d"); + foo.qux.put("bar.a", "ttyy"); + foo.qux.put("bar.b", "uuzz"); + + JacksonAdapter adapter = new JacksonAdapter(); + + // serialization + String serialized = adapter.serialize(foo); + String expected = "{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}}}"; + assertJsonEqualsNonStrict(expected, serialized); + + // deserialization + Foo deserialized = adapter.deserialize(serialized, Foo.class); + Assert.assertEquals("hello.world", deserialized.bar); + Assert.assertArrayEquals(new String[]{"hello", "hello.world"}, deserialized.baz.toArray()); + Assert.assertNotNull(deserialized.qux); + Assert.assertEquals("world", deserialized.qux.get("hello")); + Assert.assertEquals("c.d", deserialized.qux.get("a.b")); + Assert.assertEquals("ttyy", deserialized.qux.get("bar.a")); + Assert.assertEquals("uuzz", deserialized.qux.get("bar.b")); + } + + @Test + public void canSerializeMapKeysWithDotAndSlash() throws Exception { + String serialized = new JacksonAdapter().serialize(prepareSchoolModel()); + String expected = "{\"teacher\":{\"students\":{\"af.B/D\":{},\"af.B/C\":{}}},\"tags\":{\"foo.aa\":\"bar\",\"x.y\":\"zz\"},\"properties\":{\"name\":\"school1\"}}"; + assertJsonEqualsNonStrict(expected, serialized); + } + + /** + * Validates decoding and encoding of a type with type id containing dot and no additional properties + * For decoding and encoding base type will be used. + * + * @throws IOException + */ + @Test + public void canHandleTypeWithTypeIdContainingDotAndNoProperties() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + + String rabbitSerialized = "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}"; + String shelterSerialized = "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}},{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}}]}}"; + + AnimalWithTypeIdContainingDot rabbitDeserialized = adapter.deserialize(rabbitSerialized, AnimalWithTypeIdContainingDot.class); + Assert.assertTrue(rabbitDeserialized instanceof RabbitWithTypeIdContainingDot); + Assert.assertNotNull(rabbitDeserialized); + + AnimalShelter shelterDeserialized = adapter.deserialize(shelterSerialized, AnimalShelter.class); + Assert.assertTrue(shelterDeserialized instanceof AnimalShelter); + Assert.assertEquals(2, shelterDeserialized.animalsInfo().size()); + for (FlattenableAnimalInfo animalInfo: shelterDeserialized.animalsInfo()) { + Assert.assertTrue(animalInfo.animal() instanceof RabbitWithTypeIdContainingDot); + Assert.assertNotNull(animalInfo.animal()); + } + } + + /** + * Validates that decoding and encoding of a type with type id containing dot and can be done. + * For decoding and encoding base type will be used. + * + * @throws IOException + */ + @Test + public void canHandleTypeWithTypeIdContainingDot0() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // Serialize + // + List meals = new ArrayList<>(); + meals.add("carrot"); + meals.add("apple"); + // + AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + String serialized = adapter.serialize(animalToSerialize); + // + String[] results = { + "{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}" + }; + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // De-Serialize + // + AnimalWithTypeIdContainingDot animalDeserialized = adapter.deserialize(serialized, AnimalWithTypeIdContainingDot.class); + Assert.assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot); + RabbitWithTypeIdContainingDot rabbit = (RabbitWithTypeIdContainingDot) animalDeserialized; + Assert.assertNotNull(rabbit.meals()); + Assert.assertEquals(rabbit.meals().size(), 2); + } + + /** + * Validates that decoding and encoding of a type with type id containing dot and can be done. + * For decoding and encoding concrete type will be used. + * + * @throws IOException + */ + @Test + public void canHandleTypeWithTypeIdContainingDot1() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // Serialize + // + List meals = new ArrayList<>(); + meals.add("carrot"); + meals.add("apple"); + // + RabbitWithTypeIdContainingDot rabbitToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + String serialized = adapter.serialize(rabbitToSerialize); + // + String[] results = { + "{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}" + }; + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // De-Serialize + // + RabbitWithTypeIdContainingDot rabbitDeserialized = adapter.deserialize(serialized, RabbitWithTypeIdContainingDot.class); + Assert.assertTrue(rabbitDeserialized instanceof RabbitWithTypeIdContainingDot); + Assert.assertNotNull(rabbitDeserialized.meals()); + Assert.assertEquals(rabbitDeserialized.meals().size(), 2); + } + + + /** + * Validates that decoding and encoding of a type with flattenable property and type id containing dot and can be done. + * For decoding and encoding base type will be used. + * + * @throws IOException + */ + @Test + public void canHandleTypeWithFlattenablePropertyAndTypeIdContainingDot0() throws IOException { + AnimalWithTypeIdContainingDot animalToSerialize = new DogWithTypeIdContainingDot().withBreed("AKITA").withCuteLevel(10); + JacksonAdapter adapter = new JacksonAdapter(); + // serialization + String serialized = adapter.serialize(animalToSerialize); + String[] results = { + "{\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10}}", + "{\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10}}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + }; + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // de-serialization + AnimalWithTypeIdContainingDot animalDeserialized = adapter.deserialize(serialized, AnimalWithTypeIdContainingDot.class); + Assert.assertTrue(animalDeserialized instanceof DogWithTypeIdContainingDot); + DogWithTypeIdContainingDot dogDeserialized = (DogWithTypeIdContainingDot) animalDeserialized; + Assert.assertNotNull(dogDeserialized); + Assert.assertEquals(dogDeserialized.breed(), "AKITA"); + Assert.assertEquals(dogDeserialized.cuteLevel(), (Integer) 10); + } + + /** + * Validates that decoding and encoding of a type with flattenable property and type id containing dot and can be done. + * For decoding and encoding concrete type will be used. + * + * @throws IOException + */ + @Test + public void canHandleTypeWithFlattenablePropertyAndTypeIdContainingDot1() throws IOException { + DogWithTypeIdContainingDot dogToSerialize = new DogWithTypeIdContainingDot().withBreed("AKITA").withCuteLevel(10); + JacksonAdapter adapter = new JacksonAdapter(); + // serialization + String serialized = adapter.serialize(dogToSerialize); + String[] results = { + "{\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10}}", + "{\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10}}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + }; + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // de-serialization + DogWithTypeIdContainingDot dogDeserialized = adapter.deserialize(serialized, DogWithTypeIdContainingDot.class); + Assert.assertNotNull(dogDeserialized); + Assert.assertEquals(dogDeserialized.breed(), "AKITA"); + Assert.assertEquals(dogDeserialized.cuteLevel(), (Integer) 10); + } + + /** + * Validates that decoding and encoding of a array of type with type id containing dot and can be done. + * For decoding and encoding base type will be used. + * + * @throws IOException + */ + @Test + public void canHandleArrayOfTypeWithTypeIdContainingDot0() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // Serialize + // + List meals = new ArrayList<>(); + meals.add("carrot"); + meals.add("apple"); + // + AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + List animalsToSerialize = new ArrayList<>(); + animalsToSerialize.add(animalToSerialize); + String serialized = adapter.serialize(animalsToSerialize); + String[] results = { + "[{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}]", + "[{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}]", + }; + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // De-serialize + // + List animalsDeserialized = adapter.deserialize(serialized, new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[] { AnimalWithTypeIdContainingDot.class }; + } + + @Override + public Type getRawType() { + return List.class; + } + + @Override + public Type getOwnerType() { + return null; + } + }); + Assert.assertNotNull(animalsDeserialized); + Assert.assertEquals(1, animalsDeserialized.size()); + AnimalWithTypeIdContainingDot animalDeserialized = animalsDeserialized.get(0); + Assert.assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot); + RabbitWithTypeIdContainingDot rabbitDeserialized = (RabbitWithTypeIdContainingDot) animalDeserialized; + Assert.assertNotNull(rabbitDeserialized.meals()); + Assert.assertEquals(rabbitDeserialized.meals().size(), 2); + } + + /** + * Validates that decoding and encoding of a array of type with type id containing dot and can be done. + * For decoding and encoding concrete type will be used. + * + * @throws IOException + */ + @Test + public void canHandleArrayOfTypeWithTypeIdContainingDot1() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // Serialize + // + List meals = new ArrayList<>(); + meals.add("carrot"); + meals.add("apple"); + // + RabbitWithTypeIdContainingDot rabbitToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + List rabbitsToSerialize = new ArrayList<>(); + rabbitsToSerialize.add(rabbitToSerialize); + String serialized = adapter.serialize(rabbitsToSerialize); + String[] results = { + "[{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}]", + "[{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}]", + }; + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // De-serialize + // + List rabbitsDeserialized = adapter.deserialize(serialized, new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[] { RabbitWithTypeIdContainingDot.class }; + } + + @Override + public Type getRawType() { + return List.class; + } + + @Override + public Type getOwnerType() { + return null; + } + }); + Assert.assertNotNull(rabbitsDeserialized); + Assert.assertEquals(1, rabbitsDeserialized.size()); + RabbitWithTypeIdContainingDot rabbitDeserialized = rabbitsDeserialized.get(0); + Assert.assertNotNull(rabbitDeserialized.meals()); + Assert.assertEquals(rabbitDeserialized.meals().size(), 2); + } + + + /** + * Validates that decoding and encoding of a composed type with type id containing dot and can be done. + * + * @throws IOException + */ + @Test + public void canHandleComposedTypeWithTypeIdContainingDot0() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // serialization + // + List meals = new ArrayList<>(); + meals.add("carrot"); + meals.add("apple"); + AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + FlattenableAnimalInfo animalInfoToSerialize = new FlattenableAnimalInfo().withAnimal(animalToSerialize); + List animalsInfoSerialized = ImmutableList.of(animalInfoToSerialize); + AnimalShelter animalShelterToSerialize = new AnimalShelter().withAnimalsInfo(animalsInfoSerialized); + String serialized = adapter.serialize(animalShelterToSerialize); + String[] results = { + "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}}]}}", + "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}}]}}", + }; + + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // de-serialization + // + AnimalShelter shelterDeserialized = adapter.deserialize(serialized, AnimalShelter.class); + Assert.assertNotNull(shelterDeserialized.animalsInfo()); + Assert.assertEquals(shelterDeserialized.animalsInfo().size(), 1); + FlattenableAnimalInfo animalsInfoDeserialized = shelterDeserialized.animalsInfo().get(0); + Assert.assertTrue(animalsInfoDeserialized.animal() instanceof RabbitWithTypeIdContainingDot); + AnimalWithTypeIdContainingDot animalDeserialized = animalsInfoDeserialized.animal(); + Assert.assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot); + RabbitWithTypeIdContainingDot rabbitDeserialized = (RabbitWithTypeIdContainingDot) animalDeserialized; + Assert.assertNotNull(rabbitDeserialized); + Assert.assertNotNull(rabbitDeserialized.meals()); + Assert.assertEquals(rabbitDeserialized.meals().size(), 2); + } + + @Test + public void canHandleComposedSpecificPolymorphicTypeWithTypeId() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = adapter.deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet1()); + Assert.assertEquals(2, composedTurtleDeserialized.turtlesSet1().size()); + // + adapter.serialize(composedTurtleDeserialized); + // + // -- Validate scalar property + // + String serializedScalarWithTypeId = "{\"turtlesSet1Lead\":{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}}"; + // de-serialization + // + composedTurtleDeserialized = adapter.deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet1Lead()); + Assert.assertEquals(10 , (long) composedTurtleDeserialized.turtlesSet1Lead().size()); + Assert.assertEquals(100 , (long) composedTurtleDeserialized.turtlesSet1Lead().age()); + // + adapter.serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedSpecificPolymorphicTypeWithoutTypeId() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10 },{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = adapter.deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet1()); + Assert.assertEquals(2, composedTurtleDeserialized.turtlesSet1().size()); + // + adapter.serialize(composedTurtleDeserialized); + // + // -- Validate scalar property + // + String serializedScalarWithTypeId = "{\"turtlesSet1Lead\":{\"age\":100,\"size\":10 }}"; + // de-serialization + // + composedTurtleDeserialized = adapter.deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet1Lead()); + Assert.assertEquals(100 , (long) composedTurtleDeserialized.turtlesSet1Lead().age()); + // + adapter.serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedSpecificPolymorphicTypeWithAndWithoutTypeId() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = adapter.deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet1()); + Assert.assertEquals(2, composedTurtleDeserialized.turtlesSet1().size()); + // + adapter.serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedGenericPolymorphicTypeWithTypeId() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = adapter.deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2()); + Assert.assertEquals(2, composedTurtleDeserialized.turtlesSet2().size()); + // + Assert.assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot); + Assert.assertTrue(composedTurtleDeserialized.turtlesSet2().get(1) instanceof TurtleWithTypeIdContainingDot); + // + adapter.serialize(composedTurtleDeserialized); + // + // -- Validate scalar property + // + String serializedScalarWithTypeId = "{\"turtlesSet2Lead\":{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}}"; + // de-serialization + // + composedTurtleDeserialized = adapter.deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2Lead()); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2Lead() instanceof TurtleWithTypeIdContainingDot); + Assert.assertEquals(10 , (long) ((TurtleWithTypeIdContainingDot) composedTurtleDeserialized.turtlesSet2Lead()).size()); + Assert.assertEquals(100 , (long) composedTurtleDeserialized.turtlesSet2Lead().age()); + // + adapter.serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedGenericPolymorphicTypeWithoutTypeId() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10 },{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = adapter.deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2()); + Assert.assertEquals(2, composedTurtleDeserialized.turtlesSet2().size()); + // + Assert.assertFalse(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot); + Assert.assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof NonEmptyAnimalWithTypeIdContainingDot); + Assert.assertFalse(composedTurtleDeserialized.turtlesSet2().get(1) instanceof TurtleWithTypeIdContainingDot); + Assert.assertTrue(composedTurtleDeserialized.turtlesSet2().get(1) instanceof NonEmptyAnimalWithTypeIdContainingDot); + // + // -- Validate scalar property + // + adapter.serialize(composedTurtleDeserialized); + // + String serializedScalarWithTypeId = "{\"turtlesSet2Lead\":{\"age\":100,\"size\":10 }}"; + // de-serialization + // + composedTurtleDeserialized = adapter.deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2Lead()); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2Lead() instanceof NonEmptyAnimalWithTypeIdContainingDot); + // + adapter.serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedGenericPolymorphicTypeWithAndWithoutTypeId() throws IOException { + JacksonAdapter adapter = new JacksonAdapter(); + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = adapter.deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + Assert.assertNotNull(composedTurtleDeserialized); + Assert.assertNotNull(composedTurtleDeserialized.turtlesSet2()); + Assert.assertEquals(2, composedTurtleDeserialized.turtlesSet2().size()); + // + Assert.assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot); + Assert.assertTrue(composedTurtleDeserialized.turtlesSet2().get(1) instanceof NonEmptyAnimalWithTypeIdContainingDot); + // + adapter.serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleEscapedProperties() throws IOException { + FlattenedProduct productToSerialize = new FlattenedProduct(); + productToSerialize.withProductName("drink"); + productToSerialize.withPType("chai"); + JacksonAdapter adapter = new JacksonAdapter(); + // serialization + // + String serialized = adapter.serialize(productToSerialize); + String[] results = { + "{\"properties\":{\"p.name\":\"drink\",\"type\":\"chai\"}}", + "{\"properties\":{\"type\":\"chai\",\"p.name\":\"drink\"}}", + }; + + boolean found = false; + for (String result : results) { + if (result.equals(serialized)) { + found = true; + break; + } + } + Assert.assertTrue(found); + // de-serialization + // + FlattenedProduct productDeserialized = adapter.deserialize(serialized, FlattenedProduct.class); + Assert.assertNotNull(productDeserialized); + Assert.assertEquals(productDeserialized.productName(), "drink"); + Assert.assertEquals(productDeserialized.productType, "chai"); + } + + @JsonFlatten + private class School { + @JsonProperty(value = "teacher") + private Teacher teacher; + + @JsonProperty(value = "properties.name") + private String name; + + @JsonProperty(value = "tags") + private Map tags; + + public School withTeacher(Teacher teacher) { + this.teacher = teacher; + return this; + } + + public School withName(String name) { + this.name = name; + return this; + } + + public School withTags(Map tags) { + this.tags = tags; + return this; + } + } + + private class Student { + } + + private class Teacher { + @JsonProperty(value = "students") + private Map students; + + public Teacher withStudents(Map students) { + this.students = students; + return this; + } + } + + private School prepareSchoolModel() { + Teacher teacher = new Teacher(); + + Map students = new HashMap(); + students.put("af.B/C", new Student()); + students.put("af.B/D", new Student()); + + teacher.withStudents(students); + + School school = new School().withName("school1"); + school.withTeacher(teacher); + + Map schoolTags = new HashMap(); + schoolTags.put("foo.aa", "bar"); + schoolTags.put("x.y", "zz"); + + school.withTags(schoolTags); + + return school; + } + + @JsonFlatten + public static class FlattenedProduct { + // Flattened and escaped property + @JsonProperty(value = "properties.p\\.name") + private String productName; + + @JsonProperty(value = "properties.type") + private String productType; + + public String productName() { + return this.productName; + } + + public FlattenedProduct withProductName(String productName) { + this.productName = productName; + return this; + } + + public String productType() { + return this.productType; + } + + public FlattenedProduct withPType(String productType) { + this.productType = productType; + return this; + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/NonEmptyAnimalWithTypeIdContainingDot.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/NonEmptyAnimalWithTypeIdContainingDot.java new file mode 100644 index 000000000..9d5edd32f --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/NonEmptyAnimalWithTypeIdContainingDot.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = NonEmptyAnimalWithTypeIdContainingDot.class) +@JsonTypeName("NonEmptyAnimalWithTypeIdContainingDot") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "#Favourite.Pet.TurtleWithTypeIdContainingDot", value = TurtleWithTypeIdContainingDot.class) +}) +public class NonEmptyAnimalWithTypeIdContainingDot { + @JsonProperty(value = "age") + private Integer age; + + public Integer age() { + return this.age; + } + + public NonEmptyAnimalWithTypeIdContainingDot withAge(Integer age) { + this.age = age; + return this; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RabbitWithTypeIdContainingDot.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RabbitWithTypeIdContainingDot.java new file mode 100644 index 000000000..99390abdc --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RabbitWithTypeIdContainingDot.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.List; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = RabbitWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.RabbitWithTypeIdContainingDot") +public class RabbitWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { + @JsonProperty(value = "tailLength") + private Integer tailLength; + + @JsonProperty(value = "meals") + private List meals; + + public Integer filters() { + return this.tailLength; + } + + public RabbitWithTypeIdContainingDot withTailLength(Integer tailLength) { + this.tailLength = tailLength; + return this; + } + + public List meals() { + return this.meals; + } + + public RabbitWithTypeIdContainingDot withMeals(List meals) { + this.meals = meals; + return this; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RestClientTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RestClientTests.java new file mode 100644 index 000000000..d9f849618 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RestClientTests.java @@ -0,0 +1,170 @@ +/** + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * + */ + +package com.microsoft.bot.restclient; + +import com.microsoft.bot.restclient.credentials.BasicAuthenticationCredentials; +import com.microsoft.bot.restclient.credentials.TokenCredentials; +import com.microsoft.bot.restclient.interceptors.UserAgentInterceptor; +import com.microsoft.bot.restclient.protocol.ResponseBuilder; +import com.microsoft.bot.restclient.protocol.SerializerAdapter; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import okhttp3.Interceptor; +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Test; +import retrofit2.Converter; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class RestClientTests { + @Test + public void defaultConfigs() { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("https://management.azure.com/") + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()) + .build(); + Assert.assertEquals("https://management.azure.com/", restClient.retrofit().baseUrl().toString()); + Assert.assertEquals(LogLevel.NONE, restClient.logLevel()); + Assert.assertTrue(restClient.responseBuilderFactory() instanceof ServiceResponseBuilder.Factory); + Assert.assertTrue(restClient.serializerAdapter() instanceof JacksonAdapter); + Assert.assertNull(restClient.credentials()); + } + + @Test + public void newBuilderKeepsConfigs() { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("http://localhost") + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()) + .withCredentials(new TokenCredentials("Bearer", "token")) + .withLogLevel(LogLevel.BASIC) + .withInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request()); + } + }) + .withUserAgent("user") + .withNetworkInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request()); + } + }) + .withConnectionTimeout(100, TimeUnit.MINUTES) + .build(); + RestClient newClient = restClient.newBuilder().build(); + Assert.assertEquals(restClient.retrofit().baseUrl().toString(), newClient.retrofit().baseUrl().toString()); + Assert.assertEquals(restClient.logLevel(), newClient.logLevel()); + Assert.assertEquals(restClient.logLevel().isPrettyJson(), newClient.logLevel().isPrettyJson()); + Assert.assertEquals(restClient.serializerAdapter(), newClient.serializerAdapter()); + Assert.assertEquals(restClient.responseBuilderFactory(), newClient.responseBuilderFactory()); + Assert.assertEquals(restClient.credentials(), newClient.credentials()); + for (Interceptor interceptor : + newClient.httpClient().interceptors()) { + if (interceptor instanceof UserAgentInterceptor) { + Assert.assertEquals("user", ((UserAgentInterceptor) interceptor).userAgent()); + } + } + Assert.assertEquals(restClient.httpClient().interceptors().size(), newClient.httpClient().interceptors().size()); + Assert.assertEquals(restClient.httpClient().networkInterceptors().size(), newClient.httpClient().networkInterceptors().size()); + Assert.assertEquals(TimeUnit.MINUTES.toMillis(100), newClient.httpClient().connectTimeoutMillis()); + } + + @Test + public void newBuilderClonesProperties() { + RestClient restClient = new RestClient.Builder() + .withBaseUrl("http://localhost") + .withSerializerAdapter(new JacksonAdapter()) + .withResponseBuilderFactory(new ServiceResponseBuilder.Factory()) + .withCredentials(new TokenCredentials("Bearer", "token")) + .withLogLevel(LogLevel.BASIC.withPrettyJson(true)) + .withInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request()); + } + }) + .withUserAgent("user") + .withNetworkInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request()); + } + }) + .withConnectionTimeout(100, TimeUnit.MINUTES) + .build(); + RestClient newClient = restClient.newBuilder() + .withBaseUrl("https://contoso.com") + .withCredentials(new BasicAuthenticationCredentials("user", "pass")) + .withLogLevel(LogLevel.BODY_AND_HEADERS) + .withUserAgent("anotheruser") + .withConnectionTimeout(200, TimeUnit.SECONDS) + .withSerializerAdapter(new SerializerAdapter() { + @Override + public Object serializer() { + return null; + } + + @Override + public Converter.Factory converterFactory() { + return retrofit2.converter.jackson.JacksonConverterFactory.create(); + } + + @Override + public String serialize(Object object) throws IOException { + return null; + } + + @Override + public String serializeRaw(Object object) { + return null; + } + + @Override + public String serializeList(List list, CollectionFormat format) { + return null; + } + + @Override + public U deserialize(String value, Type type) throws IOException { + return null; + } + }) + .withResponseBuilderFactory(new ResponseBuilder.Factory() { + @Override + public ResponseBuilder newInstance(SerializerAdapter serializerAdapter) { + return null; + } + }) + .build(); + Assert.assertNotEquals(restClient.retrofit().baseUrl().toString(), newClient.retrofit().baseUrl().toString()); + Assert.assertNotEquals(restClient.logLevel(), newClient.logLevel()); + Assert.assertNotEquals(restClient.logLevel().isPrettyJson(), newClient.logLevel().isPrettyJson()); + Assert.assertNotEquals(restClient.serializerAdapter(), newClient.serializerAdapter()); + Assert.assertNotEquals(restClient.responseBuilderFactory(), newClient.responseBuilderFactory()); + Assert.assertNotEquals(restClient.credentials(), newClient.credentials()); + for (Interceptor interceptor : + restClient.httpClient().interceptors()) { + if (interceptor instanceof UserAgentInterceptor) { + Assert.assertEquals("user", ((UserAgentInterceptor) interceptor).userAgent()); + } + } + for (Interceptor interceptor : + newClient.httpClient().interceptors()) { + if (interceptor instanceof UserAgentInterceptor) { + Assert.assertEquals("anotheruser", ((UserAgentInterceptor) interceptor).userAgent()); + } + } + Assert.assertNotEquals(restClient.httpClient().connectTimeoutMillis(), newClient.httpClient().connectTimeoutMillis()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RetryHandlerTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RetryHandlerTests.java new file mode 100644 index 000000000..73521b8e6 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/RetryHandlerTests.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import com.microsoft.bot.restclient.retry.RetryHandler; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Assert; +import org.junit.Test; +import retrofit2.Retrofit; + +import java.io.IOException; + +public class RetryHandlerTests { + @Test + public void exponentialRetryEndOn501() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + Retrofit.Builder retrofitBuilder = new Retrofit.Builder(); + clientBuilder.addInterceptor(new RetryHandler()); + clientBuilder.addInterceptor(new Interceptor() { + // Send 408, 500, 502, all retried, with a 501 ending + private int[] codes = new int[]{408, 500, 502, 501}; + private int count = 0; + + @Override + public Response intercept(Chain chain) throws IOException { + return new Response.Builder() + .request(chain.request()) + .code(codes[count++]) + .message("Error") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, retrofitBuilder) { }; + Response response = serviceClient.httpClient().newCall( + new Request.Builder().url("http://localhost").get().build()).execute(); + Assert.assertEquals(501, response.code()); + } + + @Test + public void exponentialRetryMax() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + Retrofit.Builder retrofitBuilder = new Retrofit.Builder(); + clientBuilder.addInterceptor(new RetryHandler()); + clientBuilder.addInterceptor(new Interceptor() { + // Send 500 until max retry is hit + private int count = 0; + + @Override + public Response intercept(Chain chain) throws IOException { + Assert.assertTrue(count++ < 5); + return new Response.Builder() + .request(chain.request()) + .code(500) + .message("Error") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, retrofitBuilder) { }; + Response response = serviceClient.httpClient().newCall( + new Request.Builder().url("http://localhost").get().build()).execute(); + Assert.assertEquals(500, response.code()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ServiceClientTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ServiceClientTests.java new file mode 100644 index 000000000..c2330f8a2 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ServiceClientTests.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Assert; +import org.junit.Test; +import retrofit2.Retrofit; + +import java.io.IOException; + +public class ServiceClientTests { + @Test + public void filterTests() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + Retrofit.Builder retrofitBuilder = new Retrofit.Builder(); + clientBuilder.interceptors().add(0, new FirstFilter()); + clientBuilder.interceptors().add(1, new SecondFilter()); + clientBuilder.interceptors().add(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Assert.assertEquals("1", chain.request().header("filter1")); + Assert.assertEquals("2", chain.request().header("filter2")); + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, retrofitBuilder) { }; + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + public class FirstFilter implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request().newBuilder().header("filter1", "1").build()); + } + } + + public class SecondFilter implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request().newBuilder().header("filter2", "2").build()); + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/TurtleWithTypeIdContainingDot.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/TurtleWithTypeIdContainingDot.java new file mode 100644 index 000000000..5248d154a --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/TurtleWithTypeIdContainingDot.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = TurtleWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.TurtleWithTypeIdContainingDot") +public class TurtleWithTypeIdContainingDot extends NonEmptyAnimalWithTypeIdContainingDot { + @JsonProperty(value = "size") + private Integer size; + + public Integer size() { + return this.size; + } + + public TurtleWithTypeIdContainingDot withSize(Integer size) { + this.size = size; + return this; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/UserAgentTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/UserAgentTests.java new file mode 100644 index 000000000..91fcc1cc7 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/UserAgentTests.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import com.microsoft.bot.restclient.interceptors.UserAgentInterceptor; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Assert; +import org.junit.Test; +import retrofit2.Retrofit; + +import java.io.IOException; + +public class UserAgentTests { + @Test + public void defaultUserAgentTests() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() + .addInterceptor(new UserAgentInterceptor()) + .addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("User-Agent"); + Assert.assertEquals("AutoRest-Java", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } + + @Test + public void customUserAgentTests() throws Exception { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() + .addInterceptor(new UserAgentInterceptor().withUserAgent("Awesome")) + .addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("User-Agent"); + Assert.assertEquals("Awesome", header); + return new Response.Builder() + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + }); + ServiceClient serviceClient = new ServiceClient("http://localhost", clientBuilder, new Retrofit.Builder()) { }; + Response response = serviceClient.httpClient() + .newCall(new Request.Builder().get().url("http://localhost").build()).execute(); + Assert.assertEquals(200, response.code()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ValidatorTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ValidatorTests.java new file mode 100644 index 000000000..d4ce8f47c --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/ValidatorTests.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.TextNode; + +import org.junit.Assert; +import org.junit.Test; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.fail; + +public class ValidatorTests { + @Test + public void validateInt() throws Exception { + IntWrapper body = new IntWrapper(); + body.value = 2; + body.nullable = null; + Validator.validate(body); // pass + } + + @Test + public void validateInteger() throws Exception { + IntegerWrapper body = new IntegerWrapper(); + body.value = 3; + Validator.validate(body); // pass + try { + body.value = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("value is required")); + } + } + + @Test + public void validateString() throws Exception { + StringWrapper body = new StringWrapper(); + body.value = ""; + Validator.validate(body); // pass + try { + body.value = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("value is required")); + } + } + + /** + @Test + public void validateLocalDate() throws Exception { + LocalDateWrapper body = new LocalDateWrapper(); + body.value = new LocalDate(1, 2, 3); + Validator.validate(body); // pass + try { + body.value = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("value is required")); + } + } + **/ + + @Test + public void validateList() throws Exception { + ListWrapper body = new ListWrapper(); + try { + body.list = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("list is required")); + } + body.list = new ArrayList(); + Validator.validate(body); // pass + StringWrapper wrapper = new StringWrapper(); + wrapper.value = "valid"; + body.list.add(wrapper); + Validator.validate(body); // pass + body.list.add(null); + Validator.validate(body); // pass + body.list.add(new StringWrapper()); + try { + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("list.value is required")); + } + } + + /** + @Test + public void validateMap() throws Exception { + MapWrapper body = new MapWrapper(); + try { + body.map = null; + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("map is required")); + } + body.map = new HashMap(); + Validator.validate(body); // pass + StringWrapper wrapper = new StringWrapper(); + wrapper.value = "valid"; + body.map.put(new LocalDate(1, 2, 3), wrapper); + Validator.validate(body); // pass + body.map.put(new LocalDate(1, 2, 3), null); + Validator.validate(body); // pass + body.map.put(new LocalDate(1, 2, 3), new StringWrapper()); + try { + Validator.validate(body); // fail + fail(); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("map.value is required")); + } + } + **/ + + @Test + public void validateObject() throws Exception { + Product product = new Product(); + Validator.validate(product); + } + + @Test + public void validateRecursive() throws Exception { + TextNode textNode = new TextNode("\"\""); + Validator.validate(textNode); + } + + @Test + public void validateSkipParent() throws Exception { + Child child = new Child(); + Validator.validate(child); + } + + public final class IntWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 2 LINES + public int value; + public Object nullable; + } + + public final class IntegerWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public Integer value; + } + + public final class StringWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public String value; + } + + public final class LocalDateWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public OffsetDateTime value; + } + + public final class ListWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public List list; + } + + public final class MapWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public Map map; + } + + public enum Color { + RED, + GREEN, + Blue + } + + public final class EnumWrapper { + @JsonProperty(required = true) + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 1 LINE + public Color color; + } + + public final class Product { + // CHECKSTYLE IGNORE VisibilityModifier FOR NEXT 2 LINES + public String id; + public String tag; + } + + public abstract class Parent { + @JsonProperty(required = true) + public String requiredField; + } + + @SkipParentValidation + public final class Child extends Parent { + public String optionalField; + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/util/Foo.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/util/Foo.java new file mode 100644 index 000000000..6c06f0c12 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/util/Foo.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.bot.restclient.serializer.JsonFlatten; + +import java.util.List; +import java.util.Map; + +/** + * Class for testing serialization. + */ +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type", defaultImpl = Foo.class) +@JsonTypeName("foo") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "foochild", value = FooChild.class) +}) +public class Foo { + @JsonProperty(value = "properties.bar") + public String bar; + @JsonProperty(value = "properties.props.baz") + public List baz; + @JsonProperty(value = "properties.props.q.qux") + public Map qux; + @JsonProperty(value = "props.empty") + public Integer empty; + @JsonProperty(value = "") + public Map additionalProperties; +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/util/FooChild.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/util/FooChild.java new file mode 100644 index 000000000..f45a66cf0 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/restclient/util/FooChild.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.bot.restclient.util; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.bot.restclient.serializer.JsonFlatten; + +/** + * Class for testing serialization. + */ +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type", defaultImpl = FooChild.class) +@JsonTypeName("foochild") +public class FooChild extends Foo { +} diff --git a/libraries/bot-connector/src/test/resources/bot-connector-expired.pkcs12 b/libraries/bot-connector/src/test/resources/bot-connector-expired.pkcs12 new file mode 100644 index 000000000..ac9470061 Binary files /dev/null and b/libraries/bot-connector/src/test/resources/bot-connector-expired.pkcs12 differ diff --git a/libraries/bot-connector/src/test/resources/bot-connector.key b/libraries/bot-connector/src/test/resources/bot-connector.key new file mode 100644 index 000000000..e5e43c892 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/bot-connector.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAy8VcIYWh4sF2XcZF7u+lSq7YdE8skZhmtDF5w8GoEoPscGyT +wSeWjK7u+y2g5iAZsXQwzhN1VvOHznAlLrswLaORie7ch9veFeTMT60gLL2w8e6h +RUWxNJiQXjeExwk8Fhvcq7Kpl+qM4iEHvDX6iXCjEkNJ4Ghx48j9siUKMf1T8IcZ +sE3zzaaGOU5ar8NVsf9Kz1kPCOYv8zqykB45nUDsbE/q9cEkumL8ebjd1JApDJf0 +sf/PkftMvwP69QY1CJ/achUsRqDvFGLw+ZyUyXaSdPt92H92vbNzo8hn6GuxnPHy +f/Aoxjy/o5zNocsiiJvgFBFjRBZXWhk3OJWKZwIDAQABAoIBADNiKx9Q4Uea3Uw8 +STo9OAMjH/YEWQrF0XAy4a+ZT9aLab3Xw1J7txz2p9Cy6tXc1l3HHN96TKaGdoJ6 +CQZFsZpwmqybjQS9Tr1amqKk124wz0PSltwu/MZ0ikMX4OWH0J0KnZS2Usm6HZiQ +F7FAM1MhEh3y1dg+vilgb4jSikWcVp7RbcMgwG0N9oQhbPqN+Bv/E5KBpYk1Rdwt +yjSBOdDenx+TF5RrdKYxC3ouKN0BJMgtIIkv+iGce1WMtSAHkNXrgxJ9AhDquREU +Op363fad01Ulvs4Z+IDlEhrpA6oUt5hr8lStFhrhVuDPqctAWz27+YNw5ioHbimA +U48EOzECgYEA7T5dQzKXIzrDha4Spr8KbCMvXsesehWG3RLZxhU7lnjSMAhBuKGU +hZMfjmSSBR/rr7ppMjN4LLmpjqTkhojg/p+MnK9XRSvmGaSxlS1K8hANjShhx6s7 +xWbEsKQEPbuq1m0vCAzxgGkxLpRmM9ajJhFYEoSdVbc/hhVI72mxhiUCgYEA2+GI +ig0Wpbq5D1ISwljWHhpdi4d0MeO+wtogMo46AAX4kpgCEKDmPwX2igJKJjQJyWNB +PqHXPPQJyferun27baiNA5vEDzoCRvyLqKjyLrzSS+wDszR4mYhK8naILPonmIca +9BsUDcQw7x3rzVUlOKFpbRfT0qPVa5qn3T32apsCgYEAmYJvCloj3ZHajhdSzj5z +WgFyV1vQSLbBKy9VZoy6n+TR7G6LSBKVbdEC7Do7GcHL2Us/YlJXgmkoQ7qCfGL5 +YwiODZyPVZzQKOueVK6X/gVRH3NvwakU5ehXgQzACcnzAwhnFEh7w+FNB5zSfNx3 +eNxkJqdUvu/x1KrVJMU5L1kCgYEAkIokYGOUNKOXDTwterY9IpLAVX1YY4dLmfkb +W0BlXiiOq4bjLJ0oXduEolo49f4VRN5LQGnQ/I+Lc8msiK4oLEC1Wd7mNgAzCQjw +oZFVimWzdBcUo5Plhz+xzMsgXzieGMUPcdHvD9GdPUKVBGhpTF3G2ODl7LyoCdEj +cetOdesCgYAIuxFR/89S44Je5maTMkcExZpVTm1D1Zc8EmlHQ+WPjrakZSWFx2TS +o8wUd/mCwCTLRG3S2t3eUZiEi+G9gI8bE/w7ABxNCFAlHbo0SETy7T+9XeznoFbZ +0FyvVLvXQVZhKPVTF0pYkfuHo3ofbotKbTEM62EurroU1dviRJ7Seg== +-----END RSA PRIVATE KEY----- diff --git a/libraries/bot-connector/src/test/resources/bot-connector.pkcs12 b/libraries/bot-connector/src/test/resources/bot-connector.pkcs12 new file mode 100644 index 000000000..e9854a381 Binary files /dev/null and b/libraries/bot-connector/src/test/resources/bot-connector.pkcs12 differ diff --git a/libraries/bot-connector/src/test/resources/session-records/CreateConversationWithNullParameter.json b/libraries/bot-connector/src/test/resources/session-records/CreateConversationWithNullParameter.json new file mode 100644 index 000000000..5c8f394b2 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/CreateConversationWithNullParameter.json @@ -0,0 +1,24 @@ +{ + "networkCallRecords": [ { + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21S8SG7J:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U8H8E2HSB:T03CWQ0QB\"\r\n }\r\n ],\r\n \"activity\": {\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21S8SG7J:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U8H8E2HSB:T03CWQ0QB\"\r\n },\r\n \"text\": \"TEST Create Conversation\"\r\n }\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "body": "{\r\n \"activityId\": \"1515187112.000022\",\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8PNY7UQ7\"\r\n}", + "content-type": "application/json; charset=utf-8", + "expires": "-1", + "cache-control": "no-cache", + "date": "Fri, 05 Jan 2018 21:18:31 GMT", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "request-context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by": "ASP.NET", + "strict-transport-security": "max-age=31536000", + "StatusCode": 200 + } + }], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/DeleteActivityWithNullActivityId.json b/libraries/bot-connector/src/test/resources/session-records/DeleteActivityWithNullActivityId.json new file mode 100644 index 000000000..3876318a7 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/DeleteActivityWithNullActivityId.json @@ -0,0 +1,48 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}],\"activity\":{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Delete Activity\"}}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:11 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"activityId\": \"1514572211.000260\",\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "DELETE", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities/1514572211.000260", + "Body" : "", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:12 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/DeleteActivityWithNullConversationId.json b/libraries/bot-connector/src/test/resources/session-records/DeleteActivityWithNullConversationId.json new file mode 100644 index 000000000..3876318a7 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/DeleteActivityWithNullConversationId.json @@ -0,0 +1,48 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}],\"activity\":{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Delete Activity\"}}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:11 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"activityId\": \"1514572211.000260\",\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "DELETE", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities/1514572211.000260", + "Body" : "", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:12 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullConncetionName.json b/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullConncetionName.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullConncetionName.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullResourceUrls.json b/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullResourceUrls.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullResourceUrls.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullUserId.json b/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullUserId.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetAadTokensAsync_ShouldThrowOnNullUserId.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetActivityMembersWithNullActivityId.json b/libraries/bot-connector/src/test/resources/session-records/GetActivityMembersWithNullActivityId.json new file mode 100644 index 000000000..2f54a4ec5 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetActivityMembersWithNullActivityId.json @@ -0,0 +1,25 @@ +{ + "networkCallRecords": [{ + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ],\r\n \"activity\": {\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n },\r\n \"membersAdded\": [],\r\n \"membersRemoved\": [],\r\n \"reactionsAdded\": [],\r\n \"reactionsRemoved\": [],\r\n \"text\": \"TEST Get Activity Members with null conversation id\",\r\n \"attachments\": [],\r\n \"entities\": []\r\n }\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"activityId\": \"1516641070.000334\",\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:D2369CT7C\"\r\n}", + "Content-Length": "83", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:10 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + }], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/GetActivityMembersWithNullConversationId.json b/libraries/bot-connector/src/test/resources/session-records/GetActivityMembersWithNullConversationId.json new file mode 100644 index 000000000..2f54a4ec5 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetActivityMembersWithNullConversationId.json @@ -0,0 +1,25 @@ +{ + "networkCallRecords": [{ + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ],\r\n \"activity\": {\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n },\r\n \"membersAdded\": [],\r\n \"membersRemoved\": [],\r\n \"reactionsAdded\": [],\r\n \"reactionsRemoved\": [],\r\n \"text\": \"TEST Get Activity Members with null conversation id\",\r\n \"attachments\": [],\r\n \"entities\": []\r\n }\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"activityId\": \"1516641070.000334\",\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:D2369CT7C\"\r\n}", + "Content-Length": "83", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:10 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + }], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/GetConversationMembersWithNullConversationId.json b/libraries/bot-connector/src/test/resources/session-records/GetConversationMembersWithNullConversationId.json new file mode 100644 index 000000000..7caaf7d8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetConversationMembersWithNullConversationId.json @@ -0,0 +1,25 @@ +{ + "networkCallRecords": [ { + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21S8SG7J:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U8H8E2HSB:T03CWQ0QB\"\r\n }\r\n ],\r\n \"activity\": {\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21S8SG7J:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U8H8E2HSB:T03CWQ0QB\"\r\n },\r\n \"text\": \"TEST Get Activity Members with null activity id\"\r\n }\r\n}", + "Headers": { + "User-Agent": "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"activityId\": \"1515595854.000017\",\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8PNY7UQ7\"\r\n}", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Cache-Control": "no-cache", + "Date": "Wed, 10 Jan 2018 14:50:54 GMT", + "Pragma": "no-cache", + "Server": "Microsoft-IIS/10.0", + "Set-Cookie": "ARRAffinity=cfaeda5806a3768888e707969568c07d2824ec67e3dd88e924400998c026e3ff;Path=/;HttpOnly;Domain=slack.botframework.com", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "X-Powered-By": "ASP.NET", + "Strict-Transport-Security": "max-age=31536000", + "StatusCode": 200 + } + }], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/GetConversationPagedMembers.json b/libraries/bot-connector/src/test/resources/session-records/GetConversationPagedMembers.json new file mode 100644 index 000000000..56cc6a79e --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetConversationPagedMembers.json @@ -0,0 +1,53 @@ +{ + "networkCallRecords": [ + { + "Uri": "https://slack.botframework.com/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ]\r\n}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response":{ + "date" : "Fri, 29 Dec 2017 18:30:31 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Content-Length" : "45", + "Body" : "{\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:DD6UM0YHW\"\r\n}" + } + }, + { + "Uri": "https://slack.botframework.com/v3/conversations/B21UTEF8S%3AT03CWQ0QB%3ADD6UM0YHW/pagedmembers", + "Method": "GET", + "Body": "", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response":{ + "date" : "Fri, 29 Dec 2017 18:30:31 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Content-Length" : "185", + "Body" : "{\r\n \"members\": [\r\n {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\",\r\n \"name\": \"botframeworktest\"\r\n },\r\n {\r\n \"id\": \"U3Z9ZUDK5:T03CWQ0QB\",\r\n \"name\": \"juan.ar\"\r\n }\r\n ]\r\n}" + } + } + ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetConversationPagedMembersWithInvalidConversationId.json b/libraries/bot-connector/src/test/resources/session-records/GetConversationPagedMembersWithInvalidConversationId.json new file mode 100644 index 000000000..48aad8853 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetConversationPagedMembersWithInvalidConversationId.json @@ -0,0 +1,43 @@ +{ + "networkCallRecords": [ { + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ]\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:DD6UM0YHW\"\r\n}", + "Content-Length": "45", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Server": "Microsoft-IIS/10.0", + "Strict-Transport-Security": "max-age=31536000", + "Date": "Tue, 09 Oct 2018 18:35:49 GMT", + "StatusCode": 200 + } + }, + { + "Uri": "/v3/conversations/B21UTEF8S%3AT03CWQ0QB%3ADD6UM0YHWM/pagedmembers", + "Method": "GET", + "Body": "", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"error\": {\r\n \"code\": \"ServiceError\",\r\n \"message\": \"ExecuteWithActivityContext FAILED: The specified channel was not found\"\r\n }\r\n}", + "Content-Length": "141", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Server": "Microsoft-IIS/10.0", + "Strict-Transport-Security": "max-age=31536000", + "Date": "Tue, 09 Oct 2018 18:35:49 GMT", + "StatusCode": 400 + } + }], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/GetSigninLink_ShouldThrowOnNullState.json b/libraries/bot-connector/src/test/resources/session-records/GetSigninLink_ShouldThrowOnNullState.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetSigninLink_ShouldThrowOnNullState.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetTokenStatus_ShouldThrowOnNullUserId.json b/libraries/bot-connector/src/test/resources/session-records/GetTokenStatus_ShouldThrowOnNullUserId.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetTokenStatus_ShouldThrowOnNullUserId.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/GetUserToken_ShouldReturnNullOnInvalidConnectionstring.json b/libraries/bot-connector/src/test/resources/session-records/GetUserToken_ShouldReturnNullOnInvalidConnectionstring.json new file mode 100644 index 000000000..042285e05 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/GetUserToken_ShouldReturnNullOnInvalidConnectionstring.json @@ -0,0 +1,25 @@ +{ + "networkCallRecords": [{ + "Method": "GET", + "Uri": "/api/usertoken/GetToken?userId=default-user&connectionName=mygithubconnection1", + "Body": "", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "", + "Content-Length": "0", + "Expires": "-1", + "Cache-Control": "no-cache", + "Date": "Tue, 22 May 2018 00:08:23 GMT", + "Pragma": "no-cache", + "Server": "Microsoft-IIS/10.0", + "Set-Cookie": "ARRAffinity=0f071bbc0c38ac0ae2ef389355a8918dd4593423458c60b7aadff1f36ebca65c;Path=/;HttpOnly;Domain=api.botframework.com", + "Request-Context": "appId=cid-v1:234f01b1-3f0f-49c0-b49c-c528b5cef7a8", + "X-Powered-By": "ASP.NET", + "X-Content-Type-Options": "nosniff", + "StatusCode": 404 + } + }], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/OAuthClient_ShouldNotThrowOnHttpUrl.json b/libraries/bot-connector/src/test/resources/session-records/OAuthClient_ShouldNotThrowOnHttpUrl.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/OAuthClient_ShouldNotThrowOnHttpUrl.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/OAuthClient_ShouldThrowOnNullCredentials.json b/libraries/bot-connector/src/test/resources/session-records/OAuthClient_ShouldThrowOnNullCredentials.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/OAuthClient_ShouldThrowOnNullCredentials.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullActivity.json b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullActivity.json new file mode 100644 index 000000000..d229789ac --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullActivity.json @@ -0,0 +1,49 @@ +{ + "networkCallRecords": [ + { + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ]\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:D2369CT7C\"\r\n}", + "Content-Length": "45", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:16 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + }, + { + "Uri": "/v3/conversations/B21UTEF8S:T03CWQ0QB:D2369CT7C/activities", + "Method": "POST", + "Body": "{\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n },\r\n \"membersAdded\": [],\r\n \"membersRemoved\": [],\r\n \"reactionsAdded\": [],\r\n \"reactionsRemoved\": [],\r\n \"text\": \"TEST Reply activity with null conversation id\",\r\n \"attachments\": [],\r\n \"entities\": []\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"1516641076.000646\"\r\n}", + "Content-Length": "33", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:16 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + } + ], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullActivityId.json b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullActivityId.json new file mode 100644 index 000000000..d229789ac --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullActivityId.json @@ -0,0 +1,49 @@ +{ + "networkCallRecords": [ + { + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ]\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:D2369CT7C\"\r\n}", + "Content-Length": "45", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:16 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + }, + { + "Uri": "/v3/conversations/B21UTEF8S:T03CWQ0QB:D2369CT7C/activities", + "Method": "POST", + "Body": "{\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n },\r\n \"membersAdded\": [],\r\n \"membersRemoved\": [],\r\n \"reactionsAdded\": [],\r\n \"reactionsRemoved\": [],\r\n \"text\": \"TEST Reply activity with null conversation id\",\r\n \"attachments\": [],\r\n \"entities\": []\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"1516641076.000646\"\r\n}", + "Content-Length": "33", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:16 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + } + ], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullConversationId.json b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullConversationId.json new file mode 100644 index 000000000..d229789ac --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullConversationId.json @@ -0,0 +1,49 @@ +{ + "networkCallRecords": [ + { + "Uri": "/v3/conversations", + "Method": "POST", + "Body": "{\r\n \"bot\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"members\": [\r\n {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n }\r\n ]\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"B21UTEF8S:T03CWQ0QB:D2369CT7C\"\r\n}", + "Content-Length": "45", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:16 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + }, + { + "Uri": "/v3/conversations/B21UTEF8S:T03CWQ0QB:D2369CT7C/activities", + "Method": "POST", + "Body": "{\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"B21UTEF8S:T03CWQ0QB\"\r\n },\r\n \"recipient\": {\r\n \"id\": \"U19KH8EHJ:T03CWQ0QB\"\r\n },\r\n \"membersAdded\": [],\r\n \"membersRemoved\": [],\r\n \"reactionsAdded\": [],\r\n \"reactionsRemoved\": [],\r\n \"text\": \"TEST Reply activity with null conversation id\",\r\n \"attachments\": [],\r\n \"entities\": []\r\n}", + "Headers": { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "Body": "{\r\n \"id\": \"1516641076.000646\"\r\n}", + "Content-Length": "33", + "Content-Type": "application/json; charset=utf-8", + "Expires": "-1", + "Pragma": "no-cache", + "Request-Context": "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "Strict-Transport-Security": "max-age=31536000", + "Cache-Control": "no-cache", + "Date": "Mon, 22 Jan 2018 17:11:16 GMT", + "Server": "Microsoft-IIS/10.0", + "X-Powered-By": "ASP.NET", + "StatusCode": 200 + } + } + ], + "variables": [] +} diff --git a/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullReply.json b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullReply.json new file mode 100644 index 000000000..58bd3fc3b --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/ReplyToActivityWithNullReply.json @@ -0,0 +1,70 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}]}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:15 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Send to Conversation\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:16 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572216.000048\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities/1514572216.000048", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Reply to Activity\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:16 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572216.000202\"\r\n}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/SendToConversationWithNullActivity.json b/libraries/bot-connector/src/test/resources/session-records/SendToConversationWithNullActivity.json new file mode 100644 index 000000000..b5c37fdf6 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/SendToConversationWithNullActivity.json @@ -0,0 +1,48 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}]}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:39 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Send to Conversation\",\"name\":\"activity\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:39 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572239.000123\"\r\n}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/SendToConversationWithNullConversationId.json b/libraries/bot-connector/src/test/resources/session-records/SendToConversationWithNullConversationId.json new file mode 100644 index 000000000..b5c37fdf6 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/SendToConversationWithNullConversationId.json @@ -0,0 +1,48 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}]}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:39 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Send to Conversation\",\"name\":\"activity\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:30:39 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572239.000123\"\r\n}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/SignOutUser_ShouldThrowOnEmptyUserId.json b/libraries/bot-connector/src/test/resources/session-records/SignOutUser_ShouldThrowOnEmptyUserId.json new file mode 100644 index 000000000..ba5f37f8f --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/SignOutUser_ShouldThrowOnEmptyUserId.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullActivity.json b/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullActivity.json new file mode 100644 index 000000000..9c322cf33 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullActivity.json @@ -0,0 +1,70 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}]}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:32 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Send to Conversation\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:32 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572172.000187\"\r\n}" + } + }, { + "Method" : "PUT", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities/1514572172.000187", + "Body" : "{\"type\":\"message\",\"id\":\"1514572172.000187\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Update Activity\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:33 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572172.000187\"\r\n}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullActivityId.json b/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullActivityId.json new file mode 100644 index 000000000..9c322cf33 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullActivityId.json @@ -0,0 +1,70 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}]}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:32 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Send to Conversation\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:32 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572172.000187\"\r\n}" + } + }, { + "Method" : "PUT", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities/1514572172.000187", + "Body" : "{\"type\":\"message\",\"id\":\"1514572172.000187\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Update Activity\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:33 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572172.000187\"\r\n}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullConversationId.json b/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullConversationId.json new file mode 100644 index 000000000..9c322cf33 --- /dev/null +++ b/libraries/bot-connector/src/test/resources/session-records/UpdateActivityWithNullConversationId.json @@ -0,0 +1,70 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations", + "Body" : "{\"bot\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"members\":[{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"}]}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:32 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"B21S8SG7J:T03CWQ0QB:D8K7XGZU3\"\r\n}" + } + }, { + "Method" : "POST", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities", + "Body" : "{\"type\":\"message\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Send to Conversation\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:32 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572172.000187\"\r\n}" + } + }, { + "Method" : "PUT", + "Uri" : "https://slack.botframework.com/v3/conversations/B21S8SG7J:T03CWQ0QB:D8K7XGZU3/activities/1514572172.000187", + "Body" : "{\"type\":\"message\",\"id\":\"1514572172.000187\",\"from\":{\"id\":\"B21S8SG7J:T03CWQ0QB\"},\"recipient\":{\"id\":\"U3Z9ZUDK5:T03CWQ0QB\"},\"text\":\"TEST Update Activity\"}", + "Headers" : { + "User-Agent" : "Azure-SDK-For-Java/null OS:Windows 10/10.0 MacAddressHash:9e47c26c664df3d17fb33da29e81da7dc1985b292e3d49ee55b301a3f3f92046 Java:1.8.0_151 (BotConnector, 3.0)" + }, + "Response" : { + "date" : "Fri, 29 Dec 2017 18:29:33 GMT", + "server" : "Microsoft-IIS/10.0", + "expires" : "-1", + "vary" : "Accept-Encoding", + "retry-after" : "0", + "StatusCode" : "200", + "pragma" : "no-cache", + "strict-transport-security" : "max-age=31536000", + "request-context" : "appId=cid-v1:6814484e-c0d5-40ea-9dba-74ff29ca4f62", + "x-powered-by" : "ASP.NET", + "content-type" : "application/json; charset=utf-8", + "cache-control" : "no-cache", + "Body" : "{\r\n \"id\": \"1514572172.000187\"\r\n}" + } + } ], + "variables" : [ ] +} \ No newline at end of file diff --git a/libraries/bot-dialogs/pom.xml b/libraries/bot-dialogs/pom.xml new file mode 100644 index 000000000..2e337cbf2 --- /dev/null +++ b/libraries/bot-dialogs/pom.xml @@ -0,0 +1,165 @@ + + + 4.0.0 + + + com.microsoft.bot + bot-java + 4.15.0-SNAPSHOT + ../../pom.xml + + + bot-dialogs + jar + + ${project.groupId}:${project.artifactId} + Bot Framework Dialogs + https://dev.botframework.com/ + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + scm:git:https://github.com/Microsoft/botbuilder-java + scm:git:https://github.com/Microsoft/botbuilder-java + https://github.com/Microsoft/botbuilder-java + + + + UTF-8 + false + + + + + junit + junit + + + org.slf4j + slf4j-api + + + com.microsoft.azure + azure-documentdb + 2.6.1 + test + + + com.azure + azure-storage-blob + 12.14.1 + test + + + + com.microsoft.bot + bot-integration-core + + + com.microsoft.bot + bot-builder + ${project.version} + + + com.microsoft.bot + bot-builder + ${project.version} + test-jar + test + + + + com.google.guava + guava + + + org.javatuples + javatuples + 1.2 + + + org.apache.commons + commons-lang3 + + + org.yaml + snakeyaml + 1.32 + + + org.mockito + mockito-core + test + + + + + + build + + true + + + + + org.apache.maven.plugins + maven-pmd-plugin + + true + + com/microsoft/recognizers/** + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + com/microsoft/recognizers/** + + + + + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${pmd.version} + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + com/microsoft/recognizers/** + + + + + checkstyle + + + + + + + diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/BeginSkillDialogOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/BeginSkillDialogOptions.java new file mode 100644 index 000000000..1f472a3ba --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/BeginSkillDialogOptions.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.microsoft.bot.schema.Activity; + +/** + * A class with dialog arguments for a {@link SkillDialog} . + */ +public class BeginSkillDialogOptions { + + private Activity activity; + + /** + * Gets the {@link Activity} to send to the skill. + * @return the Activity value as a getActivity(). + */ + public Activity getActivity() { + return this.activity; + } + + /** + * Sets the {@link Activity} to send to the skill. + * @param withActivity The Activity value. + */ + public void setActivity(Activity withActivity) { + this.activity = withActivity; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java new file mode 100644 index 000000000..cefcc8e8b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; + +/** + * A {@link Dialog} that is composed of other dialogs. + * + * A component dialog has an inner {@link DialogSet} and {@link DialogContext} + * ,which provides an inner dialog stack that is hidden from the parent dialog. + */ +public class ComponentDialog extends DialogContainer { + + private String initialDialogId; + + /** + * The id for the persisted dialog state. + */ + public static final String PERSISTEDDIALOGSTATE = "dialogs"; + + private boolean initialized; + + /** + * Initializes a new instance of the {@link ComponentDialog} class. + * + * @param dialogId The D to assign to the new dialog within the parent dialog + * set. + */ + public ComponentDialog(String dialogId) { + super(dialogId); + } + + /** + * Called when the dialog is started and pushed onto the parent's dialog stack. + * + * @param outerDc The parent {@link DialogContext} for the current turn of + * conversation. + * @param options Optional, initial information to pass to the dialog. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + @Override + public CompletableFuture beginDialog(DialogContext outerDc, Object options) { + + if (outerDc == null) { + return Async.completeExceptionally(new IllegalArgumentException("outerDc cannot be null.")); + } + + return ensureInitialized(outerDc).thenCompose(ensureResult -> { + return this.checkForVersionChange(outerDc).thenCompose(checkResult -> { + DialogContext innerDc = this.createChildContext(outerDc); + return onBeginDialog(innerDc, options).thenCompose(turnResult -> { + // Check for end of inner dialog + if (turnResult.getStatus() != DialogTurnStatus.WAITING) { + // Return result to calling dialog + return endComponent(outerDc, turnResult.getResult()) + .thenCompose(result -> CompletableFuture.completedFuture(result)); + } + getTelemetryClient().trackDialogView(getId(), null, null); + // Just signal waiting + return CompletableFuture.completedFuture(END_OF_TURN); + }); + }); + }); + } + + /** + * Called when the dialog is _continued_, where it is the active dialog and the + * user replies with a new activity. + * + * @param outerDc The parent {@link DialogContext} for the current turn of + * conversation. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * result may also contain a return value. If this method is *not* + * overridden, the component dialog calls the + * {@link DialogContext#continueDialog(CancellationToken)} method on its + * inner dialog context. If the inner dialog stack is empty, the + * component dialog ends, and if a {@link DialogTurnResult#result} is + * available, the component dialog uses that as its return value. + */ + @Override + public CompletableFuture continueDialog(DialogContext outerDc) { + return ensureInitialized(outerDc).thenCompose(ensureResult -> { + return this.checkForVersionChange(outerDc).thenCompose(checkResult -> { + // Continue execution of inner dialog + DialogContext innerDc = this.createChildContext(outerDc); + return this.onContinueDialog(innerDc).thenCompose(turnResult -> { + // Check for end of inner dialog + if (turnResult.getStatus() != DialogTurnStatus.WAITING) { + // Return to calling dialog + return this.endComponent(outerDc, turnResult.getResult()) + .thenCompose(result -> CompletableFuture.completedFuture(result)); + } + + // Just signal waiting + return CompletableFuture.completedFuture(END_OF_TURN); + + }); + }); + + }); + } + + /** + * Called when a child dialog on the parent's dialog stack completed this turn, + * returning control to this dialog component. + * + * @param outerDc The {@link DialogContext} for the current turn of + * conversation. + * @param reason Reason why the dialog resumed. + * @param result Optional, value returned from the dialog that was called. The + * type of the value returned is dependent on the child dialog. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether this dialog + * is still active after this dialog turn has been processed. Generally, + * the child dialog was started with a call to + * BeginDialog(DialogContext, Object) in the parent's context. + * However, if the {@link DialogContext#replaceDialog(String, Object)} + * method is called, the logical child dialog may be different than the + * original. If this method is *not* overridden, the dialog + * automatically calls its RepromptDialog(TurnContext, + * DialogInstance) when the user replies. + */ + @Override + public CompletableFuture resumeDialog(DialogContext outerDc, DialogReason reason, Object result) { + return ensureInitialized(outerDc).thenCompose(ensureResult -> { + return this.checkForVersionChange(outerDc).thenCompose(versionCheckResult -> { + // Containers are typically leaf nodes on the stack but the dev is free to push + // other dialogs + // on top of the stack which will result in the container receiving an + // unexpected call to + // dialogResume() when the pushed on dialog ends. + // To avoid the container prematurely ending we need to implement this method + // and simply + // ask our inner dialog stack to re-prompt. + return repromptDialog(outerDc.getContext(), outerDc.getActiveDialog()).thenCompose(repromptResult -> { + return CompletableFuture.completedFuture(END_OF_TURN); + }); + }); + }); + } + + /** + * Called when the dialog should re-prompt the user for input. + * + * @param turnContext The context Object for this turn. + * @param instance State information for this dialog. + * + * @return A {@link CompletableFuture} representing the hronous operation. + */ + @Override + public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { + // Delegate to inner dialog. + DialogContext innerDc = this.createInnerDc(turnContext, instance); + return innerDc.repromptDialog().thenCompose(result -> onRepromptDialog(turnContext, instance)); + } + + /** + * Called when the dialog is ending. + * + * @param turnContext The context Object for this turn. + * @param instance State information associated with the instance of this + * component dialog on its parent's dialog stack. + * @param reason Reason why the dialog ended. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * When this method is called from the parent dialog's context, the + * component dialog cancels all of the dialogs on its inner dialog stack + * before ending. + */ + @Override + public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + // Forward cancel to inner dialogs + if (reason == DialogReason.CANCEL_CALLED) { + DialogContext innerDc = this.createInnerDc(turnContext, instance); + return innerDc.cancelAllDialogs().thenCompose(result -> onEndDialog(turnContext, instance, reason)); + } else { + return onEndDialog(turnContext, instance, reason); + } + } + + /** + * Adds a new {@link Dialog} to the component dialog and returns the updated + * component. + * + * @param dialog The dialog to add. + * + * @return The {@link ComponentDialog} after the operation is complete. + * + * The added dialog's {@link Dialog#telemetryClient} is set to the + * {@link DialogContainer#telemetryClient} of the component dialog. + */ + public ComponentDialog addDialog(Dialog dialog) { + this.getDialogs().add(dialog); + + if (this.getInitialDialogId() == null) { + this.setInitialDialogId(dialog.getId()); + } + + return this; + } + + /** + * Creates an inner {@link DialogContext} . + * + * @param dc The parent {@link DialogContext} . + * + * @return The created Dialog Context. + */ + @Override + public DialogContext createChildContext(DialogContext dc) { + return this.createInnerDc(dc, dc.getActiveDialog()); + } + + /** + * Ensures the dialog is initialized. + * + * @param outerDc The outer {@link DialogContext} . + * + * @return A {@link CompletableFuture} representing the hronous operation. + */ + protected CompletableFuture ensureInitialized(DialogContext outerDc) { + if (!this.initialized) { + this.initialized = true; + return onInitialize(outerDc).thenApply(result -> null); + } else { + return CompletableFuture.completedFuture(null); + } + } + + /** + * Initilizes the dialog. + * + * @param dc The {@link DialogContext} to initialize. + * + * @return A {@link CompletableFuture} representing the hronous operation. + */ + protected CompletableFuture onInitialize(DialogContext dc) { + if (this.getInitialDialogId() == null) { + Collection dialogs = getDialogs().getDialogs(); + if (dialogs.size() > 0) { + this.setInitialDialogId(dialogs.stream().findFirst().get().getId()); + } + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Called when the dialog is started and pushed onto the parent's dialog stack. + * + * @param innerDc The inner {@link DialogContext} for the current turn of + * conversation. + * @param options Optional, initial information to pass to the dialog. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. By + * default, this calls the + * {@link Dialog#beginDialog(DialogContext, Object)} method of the + * component dialog's initial dialog, as defined by + * InitialDialogId . Override this method in a derived class to + * implement interrupt logic. + */ + protected CompletableFuture onBeginDialog(DialogContext innerDc, Object options) { + return innerDc.beginDialog(getInitialDialogId(), options); + } + + /** + * Called when the dialog is _continued_, where it is the active dialog and the + * user replies with a new activity. + * + * @param innerDc The inner {@link DialogContext} for the current turn of + * conversation. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * result may also contain a return value. By default, this calls the + * currently active inner dialog's + * {@link Dialog#continueDialog(DialogContext)} method. Override this + * method in a derived class to implement interrupt logic. + */ + protected CompletableFuture onContinueDialog(DialogContext innerDc) { + return innerDc.continueDialog(); + } + + /** + * Called when the dialog is ending. + * + * @param context The context Object for this turn. + * @param instance State information associated with the inner dialog stack of + * this component dialog. + * @param reason Reason why the dialog ended. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * Override this method in a derived class to implement any additional + * logic that should happen at the component level, after all inner + * dialogs have been canceled. + */ + protected CompletableFuture onEndDialog(TurnContext context, DialogInstance instance, DialogReason reason) { + return CompletableFuture.completedFuture(null); + } + + /** + * Called when the dialog should re-prompt the user for input. + * + * @param turnContext The context Object for this turn. + * @param instance State information associated with the inner dialog stack + * of this component dialog. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * Override this method in a derived class to implement any additional + * logic that should happen at the component level, after the re-prompt + * operation completes for the inner dialog. + */ + protected CompletableFuture onRepromptDialog(TurnContext turnContext, DialogInstance instance) { + return CompletableFuture.completedFuture(null); + } + + /** + * Ends the component dialog in its parent's context. + * + * @param outerDc The parent {@link DialogContext} for the current turn of + * conversation. + * @param result Optional, value to return from the dialog component to the + * parent context. + * + * @return A task that represents the work queued to execute. + * + * If the task is successful, the result indicates that the dialog ended + * after the turn was processed by the dialog. In general, the parent + * context is the dialog or bot turn handler that started the dialog. If + * the parent is a dialog, the stack calls the parent's + * {@link Dialog#resumeDialog(DialogContext, DialogReason, Object)} + * method to return a result to the parent dialog. If the parent dialog + * does not implement `ResumeDialog`, then the parent will end, too, and + * the result is passed to the next parent context, if one exists. The + * returned {@link DialogTurnResult} contains the return value in its + * {@link DialogTurnResult#result} property. + */ + protected CompletableFuture endComponent(DialogContext outerDc, Object result) { + return outerDc.endDialog(result); + } + + private static DialogState buildDialogState(DialogInstance instance) { + DialogState state; + + if (instance.getState().containsKey(PERSISTEDDIALOGSTATE)) { + state = (DialogState) instance.getState().get(PERSISTEDDIALOGSTATE); + } else { + state = new DialogState(); + instance.getState().put(PERSISTEDDIALOGSTATE, state); + } + + if (state.getDialogStack() == null) { + state.setDialogStack(new ArrayList()); + } + + return state; + } + + private DialogContext createInnerDc(DialogContext outerDc, DialogInstance instance) { + DialogState state = buildDialogState(instance); + + return new DialogContext(this.getDialogs(), outerDc, state); + } + + // NOTE: You should only call this if you don't have a dc to work with (such as + // OnResume()) + private DialogContext createInnerDc(TurnContext turnContext, DialogInstance instance) { + DialogState state = buildDialogState(instance); + + return new DialogContext(this.getDialogs(), turnContext, state); + } + + /** + * Gets the id assigned to the initial dialog. + * + * @return the InitialDialogId value as a String. + */ + public String getInitialDialogId() { + return this.initialDialogId; + } + + /** + * Sets the id assigned to the initial dialog. + * + * @param withInitialDialogId The InitialDialogId value. + */ + public void setInitialDialogId(String withInitialDialogId) { + this.initialDialogId = withInitialDialogId; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java new file mode 100644 index 000000000..5c374b159 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Dialog.java @@ -0,0 +1,483 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.skills.SkillConversationReference; +import com.microsoft.bot.builder.skills.SkillHandler; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.SkillValidation; +import com.microsoft.bot.dialogs.memory.DialogStateManager; +import com.microsoft.bot.dialogs.memory.DialogStateManagerConfiguration; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.EndOfConversationCodes; + +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; + +/** + * Base class for all dialogs. + */ +public abstract class Dialog { + + /** + * A {@link DialogTurnResult} that indicates that the current dialog is still + * active and waiting for input from the user next turn. + */ + public static final DialogTurnResult END_OF_TURN = new DialogTurnResult(DialogTurnStatus.WAITING); + + @JsonIgnore + private BotTelemetryClient telemetryClient; + + @JsonProperty(value = "id") + private String id; + + /** + * Initializes a new instance of the Dialog class. + * + * @param dialogId The ID to assign to this dialog. + */ + public Dialog(String dialogId) { + id = dialogId; + telemetryClient = new NullBotTelemetryClient(); + } + + /** + * Gets id for the dialog. + * + * @return Id for the dialog. + */ + public String getId() { + if (StringUtils.isEmpty(id)) { + id = onComputeId(); + } + return id; + } + + /** + * Sets id for the dialog. + * + * @param withId Id for the dialog. + */ + public void setId(String withId) { + id = withId; + } + + /** + * Gets the {@link BotTelemetryClient} to use for logging. + * + * @return The BotTelemetryClient to use for logging. + */ + public BotTelemetryClient getTelemetryClient() { + return telemetryClient; + } + + /** + * Sets the {@link BotTelemetryClient} to use for logging. + * + * @param withTelemetryClient The BotTelemetryClient to use for logging. + */ + public void setTelemetryClient(BotTelemetryClient withTelemetryClient) { + telemetryClient = withTelemetryClient; + } + + /** + * Called when the dialog is started and pushed onto the dialog stack. + * + * @param dc The {@link DialogContext} for the current turn of conversation. + * @return If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + public CompletableFuture beginDialog(DialogContext dc) { + return beginDialog(dc, null); + } + + /** + * Called when the dialog is started and pushed onto the dialog stack. + * + * @param dc The {@link DialogContext} for the current turn of + * conversation. + * @param options Initial information to pass to the dialog. + * @return If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + public abstract CompletableFuture beginDialog(DialogContext dc, Object options); + + /** + * Called when the dialog is _continued_, where it is the active dialog and the + * user replies with a new activity. + * + *

+ * If this method is *not* overridden, the dialog automatically ends when the + * user replies. + *

+ * + * @param dc The {@link DialogContext} for the current turn of conversation. + * @return If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * result may also contain a return value. + */ + public CompletableFuture continueDialog(DialogContext dc) { + // By default just end the current dialog. + return dc.endDialog(null); + } + + /** + * Called when a child dialog completed this turn, returning control to this + * dialog. + * + *

+ * Generally, the child dialog was started with a call to + * {@link #beginDialog(DialogContext, Object)} However, if the + * {@link DialogContext#replaceDialog(String)} method is called, the logical + * child dialog may be different than the original. + *

+ * + *

+ * If this method is *not* overridden, the dialog automatically ends when the + * user replies. + *

+ * + * @param dc The dialog context for the current turn of the conversation. + * @param reason Reason why the dialog resumed. + * @return If the task is successful, the result indicates whether this dialog + * is still active after this dialog turn has been processed. + */ + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason) { + return resumeDialog(dc, reason, null); + } + + /** + * Called when a child dialog completed this turn, returning control to this + * dialog. + * + *

+ * Generally, the child dialog was started with a call to + * {@link #beginDialog(DialogContext, Object)} However, if the + * {@link DialogContext#replaceDialog(String, Object)} method is called, the + * logical child dialog may be different than the original. + *

+ * + *

+ * If this method is *not* overridden, the dialog automatically ends when the + * user replies. + *

+ * + * @param dc The dialog context for the current turn of the conversation. + * @param reason Reason why the dialog resumed. + * @param result Optional, value returned from the dialog that was called. The + * type of the value returned is dependent on the child dialog. + * @return If the task is successful, the result indicates whether this dialog + * is still active after this dialog turn has been processed. + */ + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + // By default just end the current dialog and return result to parent. + return dc.endDialog(result); + } + + /** + * Called when the dialog should re-prompt the user for input. + * + * @param turnContext The context object for this turn. + * @param instance State information for this dialog. + * @return A CompletableFuture representing the asynchronous operation. + */ + public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { + // No-op by default + return CompletableFuture.completedFuture(null); + } + + /** + * Called when the dialog is ending. + * + * @param turnContext The context object for this turn. + * @param instance State information associated with the instance of this + * dialog on the dialog stack. + * @param reason Reason why the dialog ended. + * @return A CompletableFuture representing the asynchronous operation. + */ + public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + // No-op by default + return CompletableFuture.completedFuture(null); + } + + /** + * Gets a unique String which represents the version of this dialog. If the + * version changes between turns the dialog system will emit a DialogChanged + * event. + * + * @return Unique String which should only change when dialog has changed in a + * way that should restart the dialog. + */ + @JsonIgnore + public String getVersion() { + return id; + } + + /** + * Called when an event has been raised, using `DialogContext.emitEvent()`, by + * either the current dialog or a dialog that the current dialog started. + * + * @param dc The dialog context for the current turn of conversation. + * @param e The event being raised. + * @return True if the event is handled by the current dialog and bubbling + * should stop. + */ + public CompletableFuture onDialogEvent(DialogContext dc, DialogEvent e) { + // Before bubble + return onPreBubbleEvent(dc, e).thenCompose(handled -> { + // Bubble as needed + if (!handled && e.shouldBubble() && dc.getParent() != null) { + return dc.getParent().emitEvent(e.getName(), e.getValue(), true, false); + } + + // just pass the handled value to the next stage + return CompletableFuture.completedFuture(handled); + }).thenCompose(handled -> { + if (!handled) { + // Post bubble + return onPostBubbleEvent(dc, e); + } + + return CompletableFuture.completedFuture(handled); + }); + } + + /** + * Called before an event is bubbled to its parent. + * + *

+ * This is a good place to perform interception of an event as returning `true` + * will prevent any further bubbling of the event to the dialogs parents and + * will also prevent any child dialogs from performing their default processing. + *

+ * + * @param dc The dialog context for the current turn of conversation. + * @param e The event being raised. + * @return Whether the event is handled by the current dialog and further + * processing should stop. + */ + protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) { + return CompletableFuture.completedFuture(false); + } + + /** + * Called after an event was bubbled to all parents and wasn't handled. + * + *

+ * This is a good place to perform default processing logic for an event. + * Returning `true` will prevent any processing of the event by child dialogs. + *

+ * + * @param dc The dialog context for the current turn of conversation. + * @param e The event being raised. + * @return Whether the event is handled by the current dialog and further + * processing should stop. + */ + protected CompletableFuture onPostBubbleEvent(DialogContext dc, DialogEvent e) { + return CompletableFuture.completedFuture(false); + } + + /** + * Computes an id for the Dialog. + * + * @return The id. + */ + protected String onComputeId() { + return this.getClass().getName(); + } + + /** + * Creates a dialog stack and starts a dialog, pushing it onto the stack. + * + * @param dialog The dialog to start. + * @param turnContext The context for the current turn of the conversation. + * @param accessor The StatePropertyAccessor accessor with which to manage + * the state of the dialog stack. + * @return A Task representing the asynchronous operation. + */ + public static CompletableFuture run(Dialog dialog, TurnContext turnContext, + StatePropertyAccessor accessor) { + DialogSet dialogSet = new DialogSet(accessor); + if (turnContext.getTurnState().get(BotTelemetryClient.class) != null) { + dialogSet.setTelemetryClient(turnContext.getTurnState().get(BotTelemetryClient.class)); + } else if (dialog.getTelemetryClient() != null) { + dialogSet.setTelemetryClient(dialog.getTelemetryClient()); + } else { + dialogSet.setTelemetryClient(new NullBotTelemetryClient()); + } + + dialogSet.add(dialog); + + return dialogSet.createContext(turnContext) + .thenCompose(dialogContext -> innerRun(turnContext, dialog.getId(), dialogContext, null)) + .thenAccept(dummy -> { + }); + } + + /** + * Shared implementation of run with Dialog and DialogManager. + * + * @param turnContext The turnContext. + * @param dialogId The Id of the Dialog. + * @param dialogContext The DialogContext. + * @param stateConfiguration The DialogStateManagerConfiguration. + * @return A DialogTurnResult. + */ + protected static CompletableFuture innerRun(TurnContext turnContext, String dialogId, + DialogContext dialogContext, DialogStateManagerConfiguration stateConfiguration) { + for (Entry entry : turnContext.getTurnState().getTurnStateServices().entrySet()) { + dialogContext.getServices().replace(entry.getKey(), entry.getValue()); + } + + DialogStateManager dialogStateManager = new DialogStateManager(dialogContext, stateConfiguration); + return dialogStateManager.loadAllScopes().thenCompose(result -> { + dialogContext.getContext().getTurnState().add(dialogStateManager); + DialogTurnResult dialogTurnResult = null; + boolean endOfTurn = false; + while (!endOfTurn) { + try { + dialogTurnResult = continueOrStart(dialogContext, dialogId, turnContext).join(); + endOfTurn = true; + } catch (Exception err) { + // fire error event, bubbling from the leaf. + boolean handled = dialogContext.emitEvent(DialogEvents.ERROR, err, true, true).join(); + + if (!handled) { + // error was NOT handled, return a result that signifies that the call was unsuccssfull + // (This will trigger the Adapter.OnError handler and end the entire dialog stack) + return Async.completeExceptionally(err); + } + } + } + return CompletableFuture.completedFuture(dialogTurnResult); + }); + + } + + private static CompletableFuture continueOrStart(DialogContext dialogContext, String dialogId, + TurnContext turnContext) { + if (DialogCommon.isFromParentToSkill(turnContext)) { + // Handle remote cancellation request from parent. + if (turnContext.getActivity().getType().equals(ActivityTypes.END_OF_CONVERSATION)) { + if (dialogContext.getStack().size() == 0) { + // No dialogs to cancel, just return. + return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.EMPTY)); + } + + DialogContext activeDialogContext = getActiveDialogContext(dialogContext); + + // Send cancellation message to the top dialog in the stack to ensure all the + // parents + // are canceled in the right order. + return activeDialogContext.cancelAllDialogs(true, null, null); + } + + // Handle a reprompt event sent from the parent. + if (turnContext.getActivity().getType().equals(ActivityTypes.EVENT) + && turnContext.getActivity().getName().equals(DialogEvents.REPROMPT_DIALOG)) { + if (dialogContext.getStack().size() == 0) { + // No dialogs to reprompt, just return. + return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.EMPTY)); + } + + return dialogContext.repromptDialog() + .thenApply(result -> new DialogTurnResult(DialogTurnStatus.WAITING)); + } + } + return dialogContext.continueDialog().thenCompose(result -> { + if (result.getStatus() == DialogTurnStatus.EMPTY) { + return dialogContext.beginDialog(dialogId, null).thenCompose(finalResult -> { + return processEOC(dialogContext, finalResult, turnContext); + }); + } + return processEOC(dialogContext, result, turnContext); + }); + } + + private static CompletableFuture processEOC(DialogContext dialogContext, DialogTurnResult result, + TurnContext turnContext) { + return sendStateSnapshotTrace(dialogContext).thenCompose(snapshotResult -> { + if ((result.getStatus() == DialogTurnStatus.COMPLETE + || result.getStatus() == DialogTurnStatus.CANCELLED) && sendEoCToParent(turnContext)) { + EndOfConversationCodes code = result.getStatus() == DialogTurnStatus.COMPLETE + ? EndOfConversationCodes.COMPLETED_SUCCESSFULLY + : EndOfConversationCodes.USER_CANCELLED; + Activity activity = new Activity(ActivityTypes.END_OF_CONVERSATION); + activity.setValue(result.getResult()); + activity.setLocale(turnContext.getActivity().getLocale()); + activity.setCode(code); + turnContext.sendActivity(activity).join(); + } + return CompletableFuture.completedFuture(result); + }); + } + + private static CompletableFuture sendStateSnapshotTrace(DialogContext dialogContext) { + String traceLabel = ""; + Object identity = dialogContext.getContext().getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); + if (identity instanceof ClaimsIdentity) { + traceLabel = SkillValidation.isSkillClaim(((ClaimsIdentity) identity).claims()) ? "Skill State" + : "Bot State"; + } + + // send trace of memory + JsonNode snapshot = getActiveDialogContext(dialogContext).getState().getMemorySnapshot(); + Activity traceActivity = Activity.createTraceActivity("BotState", + "https://www.botframework.com/schemas/botState", snapshot, traceLabel); + return dialogContext.getContext().sendActivity(traceActivity).thenApply(result -> null); + } + + /** + * Helper to determine if we should send an EoC to the parent or not. + * + * @param turnContext + * @return + */ + private static boolean sendEoCToParent(TurnContext turnContext) { + + ClaimsIdentity claimsIdentity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); + + if (claimsIdentity != null && SkillValidation.isSkillClaim(claimsIdentity.claims())) { + // EoC Activities returned by skills are bounced back to the bot by + // SkillHandler. + // In those cases we will have a SkillConversationReference instance in state. + SkillConversationReference skillConversationReference = turnContext.getTurnState() + .get(SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY); + if (skillConversationReference != null) { + // If the skillConversationReference.OAuthScope is for one of the supported + // channels, + // we are at the root and we should not send an EoC. + return skillConversationReference + .getOAuthScope() != AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + && skillConversationReference + .getOAuthScope() != GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE; + } + return true; + } + return false; + } + + // Recursively walk up the DC stack to find the active DC. + private static DialogContext getActiveDialogContext(DialogContext dialogContext) { + DialogContext child = dialogContext.getChild(); + if (child == null) { + return dialogContext; + } + + return getActiveDialogContext(child); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogCommon.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogCommon.java new file mode 100644 index 000000000..0ddcc8382 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogCommon.java @@ -0,0 +1,37 @@ +package com.microsoft.bot.dialogs; + +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.skills.SkillHandler; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.SkillValidation; + +/** + * A class to contain code that is duplicated across multiple Dialog related + * classes and can be shared through this common class. + */ +final class DialogCommon { + + private DialogCommon() { + + } + + /** + * Determine if a turnContext is from a Parent to a Skill. + * @param turnContext the turnContext. + * @return true if the turnContext is from a Parent to a Skill, false otherwise. + */ + static boolean isFromParentToSkill(TurnContext turnContext) { + if (turnContext.getTurnState().get(SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY) != null) { + return false; + } + + Object identity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); + if (identity instanceof ClaimsIdentity) { + ClaimsIdentity claimsIdentity = (ClaimsIdentity) identity; + return SkillValidation.isSkillClaim(claimsIdentity.claims()); + } else { + return false; + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContainer.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContainer.java new file mode 100644 index 000000000..b7bccf674 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContainer.java @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.Severity; +import com.microsoft.bot.builder.TurnContext; +import java.util.concurrent.CompletableFuture; + +/** + * A container for a set of Dialogs. + */ +public abstract class DialogContainer extends Dialog { + @JsonIgnore + private DialogSet dialogs = new DialogSet(); + + /** + * Creates a new instance with the default dialog id. + */ + public DialogContainer() { + super(null); + } + + /** + * Creates a new instance with the default dialog id. + * @param dialogId Id of the dialog. + */ + public DialogContainer(String dialogId) { + super(dialogId); + } + + /** + * Returns the Dialogs as a DialogSet. + * @return The DialogSet of Dialogs. + */ + public DialogSet getDialogs() { + return dialogs; + } + + + /** + * Sets the BotTelemetryClient to use for logging. When setting this property, + * all of the contained dialogs' BotTelemetryClient properties are also set. + * + * @param withTelemetryClient The BotTelemetryClient to use for logging. + */ + @Override + public void setTelemetryClient(BotTelemetryClient withTelemetryClient) { + super.setTelemetryClient(withTelemetryClient != null ? withTelemetryClient : new NullBotTelemetryClient()); + dialogs.setTelemetryClient(super.getTelemetryClient()); + } + + /** + * + * Creates an inner dialog context for the containers active child. + * + * @param dc Parents dialog context. + * @return A new dialog context for the active child. + */ + public abstract DialogContext createChildContext(DialogContext dc); + + /** + * Searches the current DialogSet for a Dialog by its ID. + * + * @param dialogId ID of the dialog to search for. + * @return The dialog if found; otherwise null + */ + public Dialog findDialog(String dialogId) { + return dialogs.find(dialogId); + } + + /** + * Called when an event has been raised, using `DialogContext.emitEvent()`, by + * either the current dialog or a dialog that the current dialog started. + * + *

+ * This override will trace version changes. + *

+ * + * @param dc The dialog context for the current turn of conversation. + * @param e The event being raised. + * @return True if the event is handled by the current dialog and bubbling + * should stop. + */ + @Override + public CompletableFuture onDialogEvent(DialogContext dc, DialogEvent e) { + return super.onDialogEvent(dc, e).thenCompose(handled -> { + // Trace unhandled "versionChanged" events. + if (!handled && e.getName().equals(DialogEvents.VERSION_CHANGED)) { + String traceMessage = String.format("Unhandled dialog event: {e.Name}. Active Dialog: %s", + dc.getActiveDialog().getId()); + + dc.getDialogs().getTelemetryClient().trackTrace(traceMessage, Severity.WARNING, null); + + return TurnContext.traceActivity(dc.getContext(), traceMessage).thenApply(response -> handled); + } + + return CompletableFuture.completedFuture(handled); + }); + } + + /** + * Returns internal version identifier for this container. + * + *

+ * DialogContainers detect changes of all sub-components in the container and + * map that to an DialogChanged event. Because they do this, DialogContainers + * "hide" the internal changes and just have the .id. This isolates changes to + * the container level unless a container doesn't handle it. To support this + * DialogContainers define a protected virtual method getInternalVersion() which + * computes if this dialog or child dialogs have changed which is then examined + * via calls to checkForVersionChange(). + *

+ * + * @return version which represents the change of the internals of this + * container. + */ + protected String getInternalVersion() { + return dialogs.getVersion(); + } + + /** + * Checks to see if a containers child dialogs have changed since the current + * dialog instance was started. + * + * This should be called at the start of `beginDialog()`, `continueDialog()`, + * and `resumeDialog()`. + * + * @param dc dialog context + * @return CompletableFuture + */ + protected CompletableFuture checkForVersionChange(DialogContext dc) { + String current = dc.getActiveDialog().getVersion(); + dc.getActiveDialog().setVersion(getInternalVersion()); + + // Check for change of previously stored hash + if (current != null && !current.equals(dc.getActiveDialog().getVersion())) { + // Give bot an opportunity to handle the change. + // - If bot handles it the changeHash will have been updated as to avoid + // triggering the + // change again. + return dc.emitEvent(DialogEvents.VERSION_CHANGED, getId(), true, false).thenApply(result -> null); + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java new file mode 100644 index 000000000..46fede141 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContext.java @@ -0,0 +1,581 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextStateCollection; +import com.microsoft.bot.dialogs.memory.DialogStateManager; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.connector.Async; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; + +/** + * Provides context for the current state of the dialog stack. + */ +public class DialogContext { + private DialogSet dialogs; + private TurnContext context; + private List stack; + private DialogContext parent; + private DialogStateManager state; + private TurnContextStateCollection services; + + /** + * Initializes a new instance of the DialogContext class from the turn context. + * @param withDialogs The dialog set to create the dialog context for. + * @param withTurnContext The current turn context. + * @param withState The state property from which to retrieve the dialog context. + */ + public DialogContext(DialogSet withDialogs, TurnContext withTurnContext, DialogState withState) { + if (withDialogs == null) { + throw new IllegalArgumentException("DialogContext, DialogSet is required."); + } + + if (withTurnContext == null) { + throw new IllegalArgumentException("DialogContext, TurnContext is required."); + } + + init(withDialogs, withTurnContext, withState); + } + + /** + * Initializes a new instance of the DialogContext class from the turn context. + * @param withDialogs The dialog set to create the dialog context for. + * @param withParentDialogContext Parent dialog context. + * @param withState Current dialog state. + */ + public DialogContext( + DialogSet withDialogs, + DialogContext withParentDialogContext, + DialogState withState + ) { + if (withParentDialogContext == null) { + throw new IllegalArgumentException("DialogContext, DialogContext is required."); + } + + init(withDialogs, withParentDialogContext.getContext(), withState); + + parent = withParentDialogContext; + + // copy parent services into this DialogContext. + services.copy(getParent().getServices()); + } + + + /** + * @param withDialogs + * @param withTurnContext + * @param withState + */ + private void init(DialogSet withDialogs, TurnContext withTurnContext, DialogState withState) { + dialogs = withDialogs; + context = withTurnContext; + stack = withState.getDialogStack(); + state = new DialogStateManager(this, null); + services = new TurnContextStateCollection(); + + ObjectPath.setPathValue(context.getTurnState(), TurnPath.ACTIVITY, context.getActivity()); + } + + /** + * Gets the set of dialogs which are active for the current dialog container. + * @return The set of dialogs which are active for the current dialog container. + */ + public DialogSet getDialogs() { + return dialogs; + } + + /** + * Gets the context for the current turn of conversation. + * @return The context for the current turn of conversation. + */ + public TurnContext getContext() { + return context; + } + + /** + * Gets the current dialog stack. + * @return The current dialog stack. + */ + public List getStack() { + return stack; + } + + /** + * Gets the parent DialogContext, if any. Used when searching for the ID of a dialog to start. + * @return The parent "DialogContext, if any. Used when searching for the ID of a dialog to start. + */ + public DialogContext getParent() { + return parent; + } + + /** + * Set the parent DialogContext. + * @param withDialogContext The DialogContext to set the parent to. + */ + public void setParent(DialogContext withDialogContext) { + parent = withDialogContext; + } + + /** + * Gets dialog context for child if there is an active child. + * @return Dialog context for child if there is an active child. + */ + public DialogContext getChild() { + DialogInstance instance = getActiveDialog(); + if (instance != null) { + Dialog dialog = findDialog(instance.getId()); + if (dialog instanceof DialogContainer) { + return ((DialogContainer) dialog).createChildContext(this); + } + } + + return null; + } + + /** + * Gets the cached instance of the active dialog on the top of the stack or null if the stack is empty. + * @return The cached instance of the active dialog on the top of the stack or null if the stack is empty. + */ + public DialogInstance getActiveDialog() { + if (stack.size() > 0) { + return stack.get(0); + } + + return null; + } + + /** + * Gets or sets the DialogStateManager which manages view of all memory scopes. + * @return DialogStateManager with unified memory view of all memory scopes. + */ + public DialogStateManager getState() { + return state; + } + + /** + * Gets the services collection which is contextual to this dialog context. + * @return Services collection. + */ + public TurnContextStateCollection getServices() { + return services; + } + + /** + * Starts a new dialog and pushes it onto the dialog stack. + * @param dialogId ID of the dialog to start. + * @return If the task is successful, the result indicates whether the dialog is still + * active after the turn has been processed by the dialog. + */ + public CompletableFuture beginDialog(String dialogId) { + return beginDialog(dialogId, null); + } + + /** + * Starts a new dialog and pushes it onto the dialog stack. + * @param dialogId ID of the dialog to start. + * @param options Optional, information to pass to the dialog being started. + * @return If the task is successful, the result indicates whether the dialog is still + * active after the turn has been processed by the dialog. + */ + public CompletableFuture beginDialog(String dialogId, Object options) { + if (StringUtils.isEmpty(dialogId)) { + return Async.completeExceptionally(new IllegalArgumentException( + "DialogContext.beginDialog, dialogId is required" + )); + } + + // Look up dialog + Dialog dialog = findDialog(dialogId); + if (dialog == null) { + return Async.completeExceptionally(new Exception(String.format( + "DialogContext.beginDialog(): A dialog with an id of '%s' wasn't found." + + " The dialog must be included in the current or parent DialogSet." + + " For example, if subclassing a ComponentDialog you can call AddDialog()" + + " within your constructor.", + dialogId + ))); + } + + // Push new instance onto stack + DialogInstance instance = new DialogInstance(dialogId, new HashMap<>()); + stack.add(0, instance); + + // Call dialog's Begin() method + return dialog.beginDialog(this, options); + } + + /** + * Helper function to simplify formatting the options for calling a prompt dialog. This helper will + * take an PromptOptions argument and then call {@link #beginDialog(String, Object)} + * + * @param dialogId ID of the prompt dialog to start. + * @param options Information to pass to the prompt dialog being started. + * @return If the task is successful, the result indicates whether the dialog is still + * active after the turn has been processed by the dialog. + */ + public CompletableFuture prompt(String dialogId, PromptOptions options) { + if (StringUtils.isEmpty(dialogId)) { + return Async.completeExceptionally(new IllegalArgumentException( + "DialogContext.prompt, dialogId is required" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "DialogContext.prompt, PromptOptions is required" + )); + } + + return beginDialog(dialogId, options); + } + + /** + * Continues execution of the active dialog, if there is one, by passing the current + * DialogContext to the active dialog's {@link Dialog#continueDialog(DialogContext)} + * method. + * + * @return If the task is successful, the result indicates whether the dialog is still + * active after the turn has been processed by the dialog. + */ + public CompletableFuture continueDialog() { + return Async.tryCompletable(() -> { + // if we are continuing and haven't emitted the activityReceived event, emit it + // NOTE: This is backward compatible way for activity received to be fired even if + // you have legacy dialog loop + if (!getContext().getTurnState().containsKey("activityReceivedEmitted")) { + getContext().getTurnState().replace("activityReceivedEmitted", true); + + // Dispatch "activityReceived" event + // - This will queue up any interruptions. + emitEvent(DialogEvents.ACTIVITY_RECEIVED, getContext().getActivity(), true, + true + ); + } + return CompletableFuture.completedFuture(null); + }) + .thenCompose(v -> { + if (getActiveDialog() != null) { + // Lookup dialog + Dialog dialog = this.findDialog(getActiveDialog().getId()); + if (dialog == null) { + throw new IllegalStateException(String.format( + "Failed to continue dialog. A dialog with id %s could not be found.", + getActiveDialog().getId() + )); + } + + // Continue dialog execution + return dialog.continueDialog(this); + } + + return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.EMPTY)); + }); + } + + /** + * Helper method that supplies a null result to {@link #endDialog(Object)}. + * + * @return If the task is successful, the result indicates that the dialog ended after the + * turn was processed by the dialog. + */ + public CompletableFuture endDialog() { + return endDialog(null); + } + + /** + * Ends a dialog by popping it off the stack and returns an optional result to the dialog's + * parent. The parent dialog is the dialog the started the on being ended via a call to + * either {@link #beginDialog(String, Object)} or {@link #prompt(String, PromptOptions)}. The + * parent dialog will have its {@link Dialog#resumeDialog(DialogContext, DialogReason, Object)} + * method invoked with any returned result. If the parent dialog hasn't implemented a + * {@link Dialog#resumeDialog(DialogContext dc, DialogReason reason)} method, then it will be + * automatically ended as well and the result passed to its parent. If there are no more parent + * dialogs on the stack then processing of the turn will end. + * + * @param result Optional, result to pass to the parent context. + * @return If the task is successful, the result indicates that the dialog ended after the + * turn was processed by the dialog. + */ + public CompletableFuture endDialog(Object result) { + // End the active dialog + return endActiveDialog(DialogReason.END_CALLED, result) + .thenCompose(v -> { + // Resume parent dialog + if (getActiveDialog() != null) { + // Lookup dialog + Dialog dialog = this.findDialog(getActiveDialog().getId()); + if (dialog == null) { + throw new IllegalStateException(String.format( + "DialogContext.endDialog(): Can't resume previous dialog. A dialog " + + "with an id of '%s' wasn't found.", + getActiveDialog().getId()) + ); + } + + // Return result to previous dialog + return dialog.resumeDialog(this, DialogReason.END_CALLED, result); + } + + return CompletableFuture.completedFuture( + new DialogTurnResult(DialogTurnStatus.COMPLETE, result) + ); + }); + } + + /** + * Helper method for {@link #cancelAllDialogs(boolean, String, Object)} that does not cancel + * parent dialogs or pass and event. + * + * @return If the task is successful, the result indicates that dialogs were canceled after the + * turn was processed by the dialog or that the stack was already empty. + */ + public CompletableFuture cancelAllDialogs() { + return cancelAllDialogs(false, null, null); + } + + /** + * Deletes any existing dialog stack thus canceling all dialogs on the stack. + * + *

In general, the parent context is the dialog or bot turn handler that started the dialog. + * If the parent is a dialog, the stack calls the parent's + * {@link Dialog#resumeDialog(DialogContext, DialogReason, Object)} + * method to return a result to the parent dialog. If the parent dialog does not implement + * {@link Dialog#resumeDialog}, then the parent will end, too, and the result is passed to the next + * parent context.

+ * + * @param cancelParents If true the cancellation will bubble up through any parent dialogs as well. + * @param eventName The event. If null, {@link DialogEvents#CANCEL_DIALOG} is used. + * @param eventValue The event value. Can be null. + * @return If the task is successful, the result indicates that dialogs were canceled after the + * turn was processed by the dialog or that the stack was already empty. + */ + public CompletableFuture cancelAllDialogs( + boolean cancelParents, + String eventName, + Object eventValue + ) { + eventName = eventName != null ? eventName : DialogEvents.CANCEL_DIALOG; + + if (!stack.isEmpty() || getParent() != null) { + // Cancel all local and parent dialogs while checking for interception + boolean notify = false; + DialogContext dialogContext = this; + + while (dialogContext != null) { + if (!dialogContext.stack.isEmpty()) { + // Check to see if the dialog wants to handle the event + if (notify) { + Boolean eventHandled = dialogContext.emitEvent(eventName, eventValue, false, false).join(); + if (eventHandled) { + break; + } + } + + // End the active dialog + dialogContext.endActiveDialog(DialogReason.CANCEL_CALLED).join(); + } else { + dialogContext = cancelParents ? dialogContext.getParent() : null; + } + + notify = true; + } + + return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.CANCELLED)); + } else { + // Stack was empty and no parent + return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.EMPTY)); + } + } + + /** + * Helper method for {@link #replaceDialog(String, Object)} that passes null for options. + * @param dialogId ID of the new dialog to start. + * @return If the task is successful, the result indicates whether the dialog is still + * active after the turn has been processed by the dialog. + */ + public CompletableFuture replaceDialog(String dialogId) { + return replaceDialog(dialogId, null); + } + + /** + * Starts a new dialog and replaces on the stack the currently active dialog with the new one. + * This is particularly useful for creating loops or redirecting to another dialog. + * @param dialogId ID of the new dialog to start. + * @param options Optional, information to pass to the dialog being started. + * @return If the task is successful, the result indicates whether the dialog is still + * active after the turn has been processed by the dialog. + */ + public CompletableFuture replaceDialog(String dialogId, Object options) { + // End the current dialog and giving the reason. + return endActiveDialog(DialogReason.REPLACE_CALLED) + .thenCompose(v -> { + ObjectPath.setPathValue(getContext().getTurnState(), "turn.__repeatDialogId", dialogId); + + // Start replacement dialog + return beginDialog(dialogId, options); + }); + } + + /** + * Calls the currently active dialog's {@link Dialog#repromptDialog(TurnContext, DialogInstance)} + * method. Used with dialogs that implement a re-prompt behavior. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture repromptDialog() { + // Emit 'repromptDialog' event + return emitEvent(DialogEvents.REPROMPT_DIALOG, null, false, false) + .thenCompose(handled -> { + if (!handled && getActiveDialog() != null) { + // Lookup dialog + Dialog dialog = this.findDialog(getActiveDialog().getId()); + if (dialog == null) { + throw new IllegalStateException(String.format( + "DialogSet.repromptDialog: Can't find A dialog with an id of '%s'.", + getActiveDialog().getId() + )); + } + + // Ask dialog to re-prompt if supported + return dialog.repromptDialog(getContext(), getActiveDialog()); + } + return CompletableFuture.completedFuture(null); + }); + } + + /** + * Find the dialog id for the given context. + * @param dialogId dialog id to find. + * @return dialog with that id, or null. + */ + public Dialog findDialog(String dialogId) { + if (dialogs != null) { + Dialog dialog = dialogs.find(dialogId); + if (dialog != null) { + return dialog; + } + } + + if (getParent() != null) { + return getParent().findDialog(dialogId); + } + + return null; + } + + + /** + * @param name Name of the event to raise. + * @return emitEvent + */ + public CompletableFuture emitEvent(String name) { + return emitEvent(name, null, true, false); + } + + /** + * @param name Name of the event to raise. + * @param value Value to send along with the event. + * @param bubble Flag to control whether the event should be bubbled to its parent if not handled locally. + * Defaults to a value of `true`. + * @param fromLeaf Whether the event is emitted from a leaf node. + * @return completedFuture + */ + public CompletableFuture emitEvent(String name, Object value, boolean bubble, boolean fromLeaf) { + // Initialize event + DialogEvent dialogEvent = new DialogEvent(); + dialogEvent.setBubble(bubble); + dialogEvent.setName(name); + dialogEvent.setValue(value); + + DialogContext dc = this; + + // Find starting dialog + if (fromLeaf) { + while (true) { + DialogContext childDc = dc.getChild(); + + if (childDc != null) { + dc = childDc; + } else { + break; + } + } + } + + // Dispatch to active dialog first + DialogInstance instance = dc.getActiveDialog(); + if (instance != null) { + Dialog dialog = dc.findDialog(instance.getId()); + + if (dialog != null) { + return dialog.onDialogEvent(dc, dialogEvent); + } + } + + return CompletableFuture.completedFuture(false); + } + + /** + * Obtain the locale in DialogContext. + * @return A String representing the current locale. + */ + public String getLocale() { + // turn.locale is the highest precedence. + String locale = (String) state.get(TurnContext.STATE_TURN_LOCALE); + if (!StringUtils.isEmpty(locale)) { + return locale; + } + + // If turn.locale was not populated, fall back to activity locale + locale = getContext().getActivity().getLocale(); + if (!StringUtils.isEmpty(locale)) { + return locale; + } + + return Locale.getDefault().toString(); + } + + /** + * @param reason + * @return CompletableFuture + */ + private CompletableFuture endActiveDialog(DialogReason reason) { + return endActiveDialog(reason, null); + } + + /** + * @param reason + * @param result + * @return CompletableFuture + */ + private CompletableFuture endActiveDialog(DialogReason reason, Object result) { + DialogInstance instance = getActiveDialog(); + if (instance == null) { + return CompletableFuture.completedFuture(null); + } + + return Async.wrapBlock(() -> dialogs.find(instance.getId())) + .thenCompose(dialog -> { + if (dialog != null) { + // Notify dialog of end + return dialog.endDialog(getContext(), instance, reason); + } + + return CompletableFuture.completedFuture(null); + }) + .thenCompose(v -> { + // Pop dialog off stack + stack.remove(0); + + // set Turn.LastResult to result + ObjectPath.setPathValue(getContext().getTurnState(), TurnPath.LAST_RESULT, result); + + return CompletableFuture.completedFuture(null); + }); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContextPath.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContextPath.java new file mode 100644 index 000000000..8470110cb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogContextPath.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Defines path for available dialog contexts. + */ +public final class DialogContextPath { + + private DialogContextPath() { + + } + + /** + * Memory Path to dialogContext's active dialog. + */ + public static final String ACTIVEDIALOG = "dialogcontext.activeDialog"; + + /** + * Memory Path to dialogContext's parent dialog. + */ + public static final String PARENT = "dialogcontext.parent"; + + /** + * Memory Path to dialogContext's stack. + */ + public static final String STACK = "dialogContext.stack"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogDependencies.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogDependencies.java new file mode 100644 index 000000000..e8c69cf7a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogDependencies.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.List; + +/** + * Enumerate child dialog dependencies so they can be added to the containers dialogset. + */ +public interface DialogDependencies { + + /** + * Enumerate child dialog dependencies so they can be added to the containers dialogset. + * @return Dialog list + */ + List getDependencies(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogEvent.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogEvent.java new file mode 100644 index 000000000..d6eaaee2b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogEvent.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Represents an event related to the "lifecycle" of the dialog. + */ +public class DialogEvent { + private boolean bubble; + private String name; + private Object value; + + /** + * Indicates whether the event will be bubbled to the parent `DialogContext` + * if not handled by the current dialog. + * @return Whether the event can be bubbled to the parent `DialogContext`. + */ + public boolean shouldBubble() { + return bubble; + } + + /** + * Sets whether the event will be bubbled to the parent `DialogContext` + * if not handled by the current dialog. + * @param withBubble Whether the event can be bubbled to the parent `DialogContext`. + */ + public void setBubble(boolean withBubble) { + bubble = withBubble; + } + + /** + * Gets name of the event being raised. + * @return Name of the event being raised. + */ + public String getName() { + return name; + } + + /** + * Sets name of the event being raised. + * @param withName Name of the event being raised. + */ + public void setName(String withName) { + name = withName; + } + + /** + * Gets optional value associated with the event. + * @return Optional value associated with the event. + */ + public Object getValue() { + return value; + } + + /** + * Sets optional value associated with the event. + * @param withValue Optional value associated with the event. + */ + public void setValue(Object withValue) { + value = withValue; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogEvents.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogEvents.java new file mode 100644 index 000000000..d2c1dc931 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogEvents.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; +/** + * Represents the events related to the "lifecycle" of the dialog. + */ +public final class DialogEvents { + private DialogEvents() { } + + /// Event fired when a dialog beginDialog() is called. + public static final String BEGIN_DIALOG = "beginDialog"; + + /// Event fired when a dialog RepromptDialog is Called. + public static final String REPROMPT_DIALOG = "repromptDialog"; + + /// Event fired when a dialog is canceled. + public static final String CANCEL_DIALOG = "cancelDialog"; + + /// Event fired when an activity is received from the adapter (or a request to reprocess an activity). + public static final String ACTIVITY_RECEIVED = "activityReceived"; + + /// Event which is fired when the system has detected that deployed code has changed the execution of dialogs + /// between turns. + public static final String VERSION_CHANGED = "versionChanged"; + + /// Event fired when there was an exception thrown in the system. + public static final String ERROR = "error"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogInstance.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogInstance.java new file mode 100644 index 000000000..55879d92f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogInstance.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; + +/** + * Contains state information associated with a Dialog on a dialog stack. + */ +public class DialogInstance { + @JsonProperty(value = "id") + private String id; + + @JsonProperty(value = "state") + private Map state; + + private int stackIndex; + private String version; + + /** + * Creates a DialogInstance with id and state. + */ + public DialogInstance() { + + } + + /** + * Creates a DialogInstance with id and state. + * @param withId The id + * @param withState The state. + */ + public DialogInstance(String withId, Map withState) { + id = withId; + state = withState; + } + + /** + * Gets the ID of the dialog. + * @return The dialog id. + */ + public String getId() { + return id; + } + + /** + * Sets the ID of the dialog. + * @param withId The dialog id. + */ + public void setId(String withId) { + id = withId; + } + + /** + * Gets the instance's persisted state. + * @return The instance's persisted state. + */ + public Map getState() { + return state; + } + + /** + * Sets the instance's persisted state. + * @param withState The instance's persisted state. + */ + public void setState(Map withState) { + state = withState; + } + + /** + * Gets stack index. Positive values are indexes within the current DC and negative values are + * indexes in the parent DC. + * @return Positive values are indexes within the current DC and negative values are indexes in + * the parent DC. + */ + public int getStackIndex() { + return stackIndex; + } + + /** + * Sets stack index. Positive values are indexes within the current DC and negative values are + * indexes in the parent DC. + * @param withStackIndex Positive values are indexes within the current DC and negative + * values are indexes in the parent DC. + */ + public void setStackIndex(int withStackIndex) { + stackIndex = withStackIndex; + } + + /** + * Gets version string. + * @return Unique string from the dialog this dialoginstance is tracking which is used + * to identify when a dialog has changed in way that should emit an event for changed content. + */ + public String getVersion() { + return version; + } + + /** + * Sets version string. + * @param withVersion Unique string from the dialog this dialoginstance is tracking which + * is used to identify when a dialog has changed in way that should emit + * an event for changed content. + */ + public void setVersion(String withVersion) { + version = withVersion; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogManager.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogManager.java new file mode 100644 index 000000000..071aeb6b8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogManager.java @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.microsoft.bot.builder.BotStateSet; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextStateCollection; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.memory.DialogStateManagerConfiguration; + +/** + * Class which runs the dialog system. + */ +public class DialogManager { + + private final String lastAccess = "_lastAccess"; + private String rootDialogId; + private final String dialogStateProperty; + + /** + * Initializes a new instance of the + * {@link com.microsoft.bot.dialogs.DialogManager} class. + * + * @param rootDialog Root dialog to use. + * @param dialogStateProperty Alternate name for the dialogState property. + * (Default is "DialogState"). + */ + public DialogManager(Dialog rootDialog, String dialogStateProperty) { + if (rootDialog != null) { + this.setRootDialog(rootDialog); + } + + this.dialogStateProperty = dialogStateProperty != null ? dialogStateProperty : "DialogState"; + } + + private ConversationState conversationState; + + /** + * Sets the ConversationState. + * + * @param withConversationState The ConversationState. + */ + public void setConversationState(ConversationState withConversationState) { + conversationState = withConversationState; + } + + /** + * Gets the ConversationState. + * + * @return The ConversationState. + */ + public ConversationState getConversationState() { + return conversationState; + } + + private UserState userState; + + /** + * Gets the UserState. + * + * @return UserState. + */ + public UserState getUserState() { + return this.userState; + } + + /** + * Sets the UserState. + * + * @param userState UserState. + */ + public void setUserState(UserState userState) { + this.userState = userState; + } + + private TurnContextStateCollection initialTurnState = new TurnContextStateCollection(); + + /** + * Gets InitialTurnState collection to copy into the TurnState on every turn. + * + * @return TurnState. + */ + public TurnContextStateCollection getInitialTurnState() { + return initialTurnState; + } + + /** + * Gets the Root Dialog. + * + * @return the Root Dialog. + */ + public Dialog getRootDialog() { + if (rootDialogId != null) { + return this.getDialogs().find(rootDialogId); + } else { + return null; + } + + } + + /** + * Sets root dialog to use to start conversation. + * + * @param dialog Root dialog to use to start conversation. + */ + public void setRootDialog(Dialog dialog) { + setDialogs(new DialogSet()); + if (dialog != null) { + rootDialogId = dialog.getId(); + getDialogs().setTelemetryClient(dialog.getTelemetryClient()); + getDialogs().add(dialog); + registerContainerDialogs(dialog, false); + } else { + rootDialogId = null; + } + } + + @JsonIgnore + private DialogSet dialogs = new DialogSet(); + + /** + * Returns the DialogSet. + * + * @return The DialogSet. + */ + public DialogSet getDialogs() { + return dialogs; + } + + /** + * Set the DialogSet. + * + * @param withDialogSet The DialogSet being provided. + */ + public void setDialogs(DialogSet withDialogSet) { + dialogs = withDialogSet; + } + + private DialogStateManagerConfiguration stateManagerConfiguration; + + /** + * Gets the DialogStateManagerConfiguration. + * + * @return The DialogStateManagerConfiguration. + */ + public DialogStateManagerConfiguration getStateManagerConfiguration() { + return this.stateManagerConfiguration; + } + + /** + * Sets the DialogStateManagerConfiguration. + * + * @param withStateManagerConfiguration The DialogStateManagerConfiguration to + * set from. + */ + public void setStateManagerConfiguration(DialogStateManagerConfiguration withStateManagerConfiguration) { + this.stateManagerConfiguration = withStateManagerConfiguration; + } + + private Integer expireAfter; + + /** + * Gets the (optinal) number of milliseconds to expire the bot's state after. + * + * @return Number of milliseconds. + */ + public Integer getExpireAfter() { + return this.expireAfter; + } + + /** + * Sets the (optional) number of milliseconds to expire the bot's state after. + * + * @param withExpireAfter Number of milliseconds. + */ + public void setExpireAfter(Integer withExpireAfter) { + this.expireAfter = withExpireAfter; + } + + /** + * Runs dialog system in the context of an ITurnContext. + * + * @param context Turn Context + * @return result of runnign the logic against the activity. + */ + public CompletableFuture onTurn(TurnContext context) { + BotStateSet botStateSet = new BotStateSet(); + + // Preload TurnState with DM TurnState. + initialTurnState.getTurnStateServices().forEach((key, value) -> { + context.getTurnState().add(key, value); + }); + + // register DialogManager with TurnState. + context.getTurnState().replace(this); + + if (conversationState == null) { + ConversationState cState = context.getTurnState().get(ConversationState.class); + if (cState != null) { + conversationState = cState; + } else { + return Async.completeExceptionally(new IllegalStateException( + String.format("Unable to get an instance of %s from turnContext.", + ConversationState.class.toString()) + )); + } + } else { + context.getTurnState().replace(conversationState); + } + + botStateSet.add(conversationState); + + if (userState == null) { + userState = context.getTurnState().get(UserState.class); + } else { + context.getTurnState().replace(userState); + } + + if (userState != null) { + botStateSet.add(userState); + } + + // create property accessors + StatePropertyAccessor lastAccessProperty = conversationState.createProperty(lastAccess); + + OffsetDateTime lastAccessed = lastAccessProperty.get(context, () -> { + return OffsetDateTime.now(ZoneId.of("UTC")); + }).join(); + + // Check for expired conversation + if (expireAfter != null && (OffsetDateTime.now(ZoneId.of("UTC")).toInstant().toEpochMilli() + - lastAccessed.toInstant().toEpochMilli()) >= expireAfter) { + conversationState.clearState(context).join(); + } + + lastAccessed = OffsetDateTime.now(ZoneId.of("UTC")); + lastAccessProperty.set(context, lastAccessed).join(); + + // get dialog stack + StatePropertyAccessor dialogsProperty = conversationState.createProperty(dialogStateProperty); + + DialogState dialogState = dialogsProperty.get(context, DialogState::new).join(); + + // Create DialogContext + DialogContext dc = new DialogContext(dialogs, context, dialogState); + + return Dialog.innerRun(context, rootDialogId, dc, getStateManagerConfiguration()).thenCompose(turnResult -> { + return botStateSet.saveAllChanges(dc.getContext(), false).thenCompose(saveResult -> { + DialogManagerResult result = new DialogManagerResult(); + result.setTurnResult(turnResult); + return CompletableFuture.completedFuture(result); + }); + }); + } + + /** + * Recursively traverses the Dialog tree and registers instances of + * DialogContainer in the DialogSet for this + * instance. + * + * @param dialog Root of the Dialog subtree to iterate and register + * containers from. + * @param registerRoot Whether to register the root of the subtree. + */ + private void registerContainerDialogs(Dialog dialog, Boolean registerRoot) { + if (dialog instanceof DialogContainer) { + if (registerRoot) { + getDialogs().add(dialog); + } + + Collection dlogs = ((DialogContainer) dialog).getDialogs().getDialogs(); + + for (Dialog dlg : dlogs) { + registerContainerDialogs(dlg, true); + } + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogManagerResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogManagerResult.java new file mode 100644 index 000000000..d73d91c28 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogManagerResult.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.microsoft.bot.schema.Activity; + +/** + * Represents the result of the Dialog Manager turn. + */ +public class DialogManagerResult { + /** + * The result returned to the caller. + */ + private DialogTurnResult turnResult; + + /** + * The array of resulting activities. + */ + private Activity[] activities; + + /** + * The resulting new state. + */ + private PersistedState newState; + + /** + * @return DialogTurnResult + */ + public DialogTurnResult getTurnResult() { + return this.turnResult; + } + + /** + * @param withTurnResult Sets the turnResult. + */ + public void setTurnResult(DialogTurnResult withTurnResult) { + this.turnResult = withTurnResult; + } + + /** + * @return Activity[] + */ + public Activity[] getActivities() { + return this.activities; + } + + /** + * @param withActivities Sets the activites. + */ + public void setActivities(Activity[] withActivities) { + this.activities = withActivities; + } + + /** + * @return PersistedState + */ + public PersistedState getNewState() { + return this.newState; + } + + /** + * @param withNewState sets the newState. + */ + public void setNewState(PersistedState withNewState) { + this.newState = withNewState; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogPath.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogPath.java new file mode 100644 index 000000000..8827644f7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogPath.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Defines path for available dialogs. + */ +public final class DialogPath { + + private DialogPath() { + + } + + /** + * Counter of emitted events. + */ + public static final String EVENTCOUNTER = "dialog.eventCounter"; + + /** + * Currently expected properties. + */ + public static final String EXPECTEDPROPERTIES = "dialog.expectedProperties"; + + /** + * Default operation to use for entities where there is no identified operation entity. + */ + public static final String DEFAULTOPERATION = "dialog.defaultOperation"; + + /** + * Last surfaced entity ambiguity event. + */ + public static final String LASTEVENT = "dialog.lastEvent"; + + /** + * Currently required properties. + */ + public static final String REQUIREDPROPERTIES = "dialog.requiredProperties"; + + /** + * Number of retries for the current Ask. + */ + public static final String RETRIES = "dialog.retries"; + + /** + * Last intent. + */ + public static final String LASTINTENT = "dialog.lastIntent"; + + /** + * Last trigger event: defined in FormEvent, ask, clarifyEntity etc. + */ + public static final String LASTTRIGGEREVENT = "dialog.lastTriggerEvent"; + + /** + * Utility function to get just the property name without the memory scope prefix. + * @param property Memory scope property path. + * @return Name of the property without the prefix. + */ + public static String getPropertyName(String property) { + return property.replace("dialog.", ""); + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogReason.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogReason.java new file mode 100644 index 000000000..efe2a152d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogReason.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Indicates in which a dialog-related method is being called. + */ +public enum DialogReason { + /// A dialog was started. + BEGIN_CALLED, + + /// A dialog was continued. + CONTINUE_CALLED, + + /// A dialog was ended normally. + END_CALLED, + + /// A dialog was ending because it was replaced. + REPLACE_CALLED, + + /// A dialog was canceled. + CANCEL_CALLED, + + /// A preceding step of the dialog was skipped. + NEXT_CALLED +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogSet.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogSet.java new file mode 100644 index 000000000..63fdcd743 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogSet.java @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.hash.Hashing; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; + +/** + * A collection of Dialog objects that can all call each other. + */ +public class DialogSet { + private Map dialogs = new HashMap<>(); + private StatePropertyAccessor dialogState; + @JsonIgnore + private BotTelemetryClient telemetryClient; + private String version; + + /** + * Initializes a new instance of the DialogSet class. + * + *

+ * To start and control the dialogs in this dialog set, create a DialogContext + * and use its methods to start, continue, or end dialogs. To create a dialog + * context, call createContext(TurnContext). + *

+ * + * @param withDialogState The state property accessor with which to manage the + * stack for this dialog set. + */ + public DialogSet(StatePropertyAccessor withDialogState) { + if (withDialogState == null) { + throw new IllegalArgumentException("DialogState is required"); + } + dialogState = withDialogState; + telemetryClient = new NullBotTelemetryClient(); + } + + /** + * Creates a DialogSet without state. + */ + public DialogSet() { + dialogState = null; + telemetryClient = new NullBotTelemetryClient(); + } + + /** + * Gets the BotTelemetryClient to use for logging. + * + * @return The BotTelemetryClient to use for logging. + */ + public BotTelemetryClient getTelemetryClient() { + return telemetryClient; + } + + /** + * Sets the BotTelemetryClient to use for logging. + * + *

+ * When this property is set, it sets the Dialog.TelemetryClient of each dialog + * in the set to the new value. + *

+ * + * @param withBotTelemetryClient The BotTelemetryClient to use for logging. + */ + public void setTelemetryClient(BotTelemetryClient withBotTelemetryClient) { + telemetryClient = withBotTelemetryClient != null ? withBotTelemetryClient : new NullBotTelemetryClient(); + + for (Dialog dialog : dialogs.values()) { + dialog.setTelemetryClient(telemetryClient); + } + } + + /** + * Gets a unique string which represents the combined versions of all dialogs in + * this dialogset. + * + * @return Version will change when any of the child dialogs version changes. + */ + public String getVersion() { + if (version == null) { + StringBuilder sb = new StringBuilder(); + for (Dialog dialog : dialogs.values()) { + String v = dialog.getVersion(); + if (!StringUtils.isEmpty(v)) { + sb.append(v); + } + } + + version = Hashing.sha256().hashString(sb.toString(), StandardCharsets.UTF_8).toString(); + } + + return version; + } + + /** + * Adds a new dialog to the set and returns the set to allow fluent chaining. If + * the Dialog.Id being added already exists in the set, the dialogs id will be + * updated to include a suffix which makes it unique. So adding 2 dialogs named + * "duplicate" to the set would result in the first one having an id of + * "duplicate" and the second one having an id of "duplicate2". + * + * @param dialog The dialog to add. The added dialog's Dialog.TelemetryClient is + * set to the BotTelemetryClient of the dialog set. + * @return The dialog set after the operation is complete. + */ + public DialogSet add(Dialog dialog) { + // Ensure new version hash is computed + version = null; + + if (dialog == null) { + throw new IllegalArgumentException("Dialog is required"); + } + + if (dialogs.containsKey(dialog.getId())) { + // If we are trying to add the same exact instance, it's not a name collision. + // No operation required since the instance is already in the dialog set. + if (dialogs.get(dialog.getId()) == dialog) { + return this; + } + + // If we are adding a new dialog with a conflicting name, add a suffix to avoid + // dialog name collisions. + int nextSuffix = 2; + + while (true) { + String suffixId = dialog.getId() + nextSuffix; + + if (!dialogs.containsKey(suffixId)) { + dialog.setId(suffixId); + break; + } + + nextSuffix++; + } + } + + dialog.setTelemetryClient(telemetryClient); + dialogs.put(dialog.getId(), dialog); + + // Automatically add any dependencies the dialog might have + if (dialog instanceof DialogDependencies) { + for (Dialog dependencyDialog : ((DialogDependencies) dialog).getDependencies()) { + add(dependencyDialog); + } + } + + return this; + } + + /** + * Creates a DialogContext which can be used to work with the dialogs in the + * DialogSet. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @return A CompletableFuture representing the asynchronous operation. + */ + public CompletableFuture createContext(TurnContext turnContext) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "TurnContext is required" + )); + } + + if (dialogState == null) { + // Note: This shouldn't ever trigger, as the _dialogState is set in the + // constructor + // and validated there. + return Async.completeExceptionally(new IllegalStateException( + "DialogSet.createContext(): DialogSet created with a null StatePropertyAccessor." + )); + } + + // Load/initialize dialog state + return dialogState.get(turnContext, DialogState::new) + .thenApply(state -> new DialogContext(this, turnContext, state)); + } + + /** + * Searches the current DialogSet for a Dialog by its ID. + * + * @param dialogId ID of the dialog to search for. + * @return The dialog if found; otherwise null + */ + public Dialog find(String dialogId) { + if (StringUtils.isEmpty(dialogId)) { + throw new IllegalArgumentException("DialogSet.find, dialogId is required"); + } + + return dialogs.get(dialogId); + } + + /** + * Returns a collection of Dialogs in this DialogSet. + * + * @return The Dialogs in this DialogSet. + */ + public Collection getDialogs() { + return dialogs.values(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java new file mode 100644 index 000000000..257889980 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains state information for the dialog stack. + */ +public class DialogState { + @JsonProperty(value = "dialogStack") + private List dialogStack; + + /** + * Initializes a new instance of the class with an empty stack. + */ + public DialogState() { + this(null); + } + + /** + * Initializes a new instance of the class. + * @param withDialogStack The state information to initialize the stack with. + */ + public DialogState(List withDialogStack) { + dialogStack = withDialogStack != null ? withDialogStack : new ArrayList(); + } + + /** + * Gets the state information for a dialog stack. + * @return State information for a dialog stack. + */ + public List getDialogStack() { + return dialogStack; + } + + /** + * Sets the state information for a dialog stack. + * @param withDialogStack State information for a dialog stack. + */ + public void setDialogStack(List withDialogStack) { + dialogStack = withDialogStack; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogTurnResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogTurnResult.java new file mode 100644 index 000000000..f4f342c42 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogTurnResult.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Result returned to the caller of one of the various stack manipulation methods. + */ +public class DialogTurnResult { + private DialogTurnStatus status; + private Object result; + private boolean parentEnded; + + /** + * Creates a DialogTurnResult with a status. + * @param withStatus The dialog status. + */ + public DialogTurnResult(DialogTurnStatus withStatus) { + this(withStatus, null); + } + + /** + * Creates a DialogTurnResult with a status and result. + * @param withStatus The dialog status. + * @param withResult The result. + */ + public DialogTurnResult(DialogTurnStatus withStatus, Object withResult) { + status = withStatus; + result = withResult; + } + + /** + * Gets the current status of the stack. + * @return The current status of the stack. + */ + public DialogTurnStatus getStatus() { + return status; + } + + /** + * Sets the current status of the stack. + * @param withStatus The current status of the stack. + */ + public void setStatus(DialogTurnStatus withStatus) { + status = withStatus; + } + + /** + * Gets or sets the result returned by a dialog that was just ended. + * + *

This will only be populated in certain cases: + *
- The bot calls `DialogContext.BeginDialogAsync()` to start a new dialog and the dialog + * ends immediately.
+ *
- The bot calls `DialogContext.ContinueDialogAsync()` and a dialog that was active ends.

+ * + *

In all cases where it's populated, {@link "DialogContext.ActiveDialog"} will be `null`.

+ * @return The result returned by a dialog that was just ended. + */ + public Object getResult() { + return result; + } + + /** + * Sets the result returned by a dialog that was just ended. + * @param withResult The result returned by a dialog that was just ended. + */ + public void setResult(Object withResult) { + result = withResult; + } + + /** + * Indicates whether a DialogCommand has ended its parent container and the parent should + * not perform any further processing. + * @return Whether a DialogCommand has ended its parent container and the parent should + * not perform any further processing. + */ + public boolean hasParentEnded() { + return parentEnded; + } + + /** + * Sets whether a DialogCommand has ended its parent container and the parent should + * not perform any further processing. + * @param withParentEnded Whether a DialogCommand has ended its parent container and the + * parent should not perform any further processing. + */ + public void setParentEnded(boolean withParentEnded) { + parentEnded = withParentEnded; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogTurnStatus.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogTurnStatus.java new file mode 100644 index 000000000..1a5a6a84d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogTurnStatus.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Result returned to the caller of one of the various stack manipulation methods. + */ +public enum DialogTurnStatus { + /// Indicates that there is currently nothing on the dialog stack. + EMPTY, + + /// Indicates that the dialog on top is waiting for a response from the user. + WAITING, + + /// Indicates that a dialog completed successfully, the result is available, and no child + /// dialogs to the current context are on the dialog stack. + COMPLETE, + + /// Indicates that the dialog was canceled, and no child + /// dialogs to the current context are on the dialog stack. + CANCELLED, + + /// Current dialog completed successfully, but turn should end. + COMPLETEANDWAIT, +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogsComponentRegistration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogsComponentRegistration.java new file mode 100644 index 000000000..750bd4cb3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogsComponentRegistration.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.ArrayList; +import java.util.List; + +import com.microsoft.bot.builder.ComponentRegistration; +import com.microsoft.bot.dialogs.memory.ComponentMemoryScopes; +import com.microsoft.bot.dialogs.memory.ComponentPathResolvers; +import com.microsoft.bot.dialogs.memory.PathResolver; +import com.microsoft.bot.dialogs.memory.pathresolvers.PercentPathResolver; +import com.microsoft.bot.dialogs.memory.pathresolvers.AtAtPathResolver; +import com.microsoft.bot.dialogs.memory.pathresolvers.AtPathResolver; +import com.microsoft.bot.dialogs.memory.pathresolvers.HashPathResolver; +import com.microsoft.bot.dialogs.memory.pathresolvers.DollarPathResolver; +import com.microsoft.bot.dialogs.memory.scopes.ClassMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.ConversationMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.DialogClassMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.DialogContextMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.DialogMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.MemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.SettingsMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.ThisMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.TurnMemoryScope; +import com.microsoft.bot.dialogs.memory.scopes.UserMemoryScope; + +/** + * Makes Dialogs components available to the system registering functionality. + */ +public class DialogsComponentRegistration extends ComponentRegistration + implements ComponentMemoryScopes, ComponentPathResolvers { + + /** + * Gets the Dialogs Path Resolvers. + */ + @Override + public Iterable getPathResolvers() { + List listToReturn = new ArrayList(); + listToReturn.add((PathResolver) new DollarPathResolver()); + listToReturn.add((PathResolver) new HashPathResolver()); + listToReturn.add((PathResolver) new AtAtPathResolver()); + listToReturn.add((PathResolver) new AtPathResolver()); + listToReturn.add((PathResolver) new PercentPathResolver()); + return listToReturn; + } + + /** + * Gets the Dialogs Memory Scopes. + */ + @Override + public Iterable getMemoryScopes() { + List listToReturn = new ArrayList(); + listToReturn.add(new TurnMemoryScope()); + listToReturn.add(new SettingsMemoryScope()); + listToReturn.add(new DialogMemoryScope()); + listToReturn.add(new DialogContextMemoryScope()); + listToReturn.add(new DialogClassMemoryScope()); + listToReturn.add(new ClassMemoryScope()); + listToReturn.add(new ThisMemoryScope()); + listToReturn.add(new ConversationMemoryScope()); + listToReturn.add(new UserMemoryScope()); + return listToReturn; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ObjectPath.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ObjectPath.java new file mode 100644 index 000000000..91e618364 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ObjectPath.java @@ -0,0 +1,821 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.TurnContextStateCollection; +import com.microsoft.bot.schema.Serialization; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + +/** + * Helper methods for working with dynamic json objects. + */ +public final class ObjectPath { + private ObjectPath() { } + + /** + * Does an Object have a subpath. + * @param obj Object. + * @param path path to evaluate. + * @return true if the path is there. + */ + public static boolean hasValue(Object obj, String path) { + return tryGetPathValue(obj, path, Object.class) != null; + } + + /** + * Get the value for a path relative to an Object. + * @param type to return. + * @param obj Object to start with. + * @param path path to evaluate. + * @param valueType Type of T + * @return value or default(T). + */ + @SuppressWarnings("checkstyle:InnerAssignment") + public static T getPathValue(Object obj, String path, Class valueType) { + T value; + if ((value = tryGetPathValue(obj, path, valueType)) != null) { + return value; + } + + throw new IllegalArgumentException(path); + } + + /** + * Get the value for a path relative to an Object. + * @param type to return. + * @param obj Object to start with. + * @param path path to evaluate. + * @param valueType type of T + * @param defaultValue default value to use if any part of the path is missing. + * @return value or default(T). + */ + @SuppressWarnings("checkstyle:InnerAssignment") + public static T getPathValue(Object obj, String path, Class valueType, T defaultValue) { + T value; + if ((value = tryGetPathValue(obj, path, valueType)) != null) { + return value; + } + + return defaultValue; + } + + /** + * Get the value for a path relative to an Object. + * @param type to return. + * @param obj Object to start with. + * @param path path to evaluate. + * @param valueType value for the path. + * @return true if successful. + */ + public static T tryGetPathValue(Object obj, String path, Class valueType) { + if (obj == null || path == null) { + return null; + } + + if (path.length() == 0) { + return mapValueTo(obj, valueType); + } + + Segments segments = tryResolvePath(obj, path); + if (segments == null) { + return null; + } + + Object result = resolveSegments(obj, segments); + if (result == null) { + return null; + } + + // look to see if it's ExpressionProperty and bind it if it is + // NOTE: this bit of duck typing keeps us from adding dependency between adaptiveExpressions and Dialogs. + /* + if (result.GetType().GetProperty("ExpressionText") != null) + { + var method = result.GetType().GetMethod("GetValue", new[] { typeof(Object) }); + if (method != null) + { + result = method.Invoke(result, new[] { obj }); + } + } + */ + + return mapValueTo(result, valueType); + } + + /** + * Given an Object evaluate a path to set the value. + * @param obj Object to start with. + * @param path path to evaluate. + * @param value value to store. + */ + public static void setPathValue(Object obj, String path, Object value) { + setPathValue(obj, path, value, true); + } + + /** + * Given an Object evaluate a path to set the value. + * @param obj Object to start with. + * @param path path to evaluate. + * @param value value to store. + * @param json if true, sets the value as primitive JSON Objects. + */ + public static void setPathValue(Object obj, String path, Object value, boolean json) { + Segments segments = tryResolvePath(obj, path); + if (segments == null) { + return; + } + + Object current = obj; + for (int i = 0; i < segments.size() - 1; i++) { + SegmentType segment = segments.getSegment(i); + Object next; + if (segment.isInt) { + if (((Segments) current).size() <= segment.intValue) { + // TODO make sure growBy is correct + // Expand array to index + int growBy = segment.intValue - ((Segments) current).size(); + ((ArrayNode) current).add(growBy); + } + next = ((ArrayNode) current).get(segment.intValue); + } else { + next = getObjectProperty(current, segment.stringValue); + if (next == null) { + // Create Object or array base on next segment + SegmentType nextSegment = new SegmentType(segments.get(i + 1)); + if (nextSegment.stringValue != null) { + setObjectSegment(current, segment.stringValue, JsonNodeFactory.instance.objectNode()); + } else { + setObjectSegment(current, segment.stringValue, JsonNodeFactory.instance.arrayNode()); + } + next = getObjectProperty(current, segment.stringValue); + } + } + + current = next; + } + + Object lastSegment = segments.last(); + setObjectSegment(current, lastSegment, value, json); + } + + /** + * Remove path from Object. + * @param obj Object to change. + * @param path Path to remove. + */ + public static void removePathValue(Object obj, String path) { + Segments segments = tryResolvePath(obj, path); + if (segments == null) { + return; + } + + Object current = obj; + for (int i = 0; i < segments.size() - 1; i++) { + Object segment = segments.get(i); + current = resolveSegment(current, segment); + if (current == null) { + return; + } + } + + if (current != null) { + Object lastSegment = segments.last(); + if (lastSegment instanceof String) { + // lastSegment is a field name + if (current instanceof Map) { + ((Map) current).remove((String) lastSegment); + } else { + ((ObjectNode) current).remove((String) lastSegment); + } + } else { + // lastSegment is an index + ((ArrayNode) current).set((int) lastSegment, (JsonNode) null); + } + } + } + + /** + * Apply an action to all properties in an Object. + * @param obj Object to map against. + * @param action Action to take. + */ + public static void forEachProperty(Object obj, BiConsumer action) { + if (obj instanceof Map) { + ((Map) obj).forEach(action); + } else if (obj instanceof ObjectNode) { + ObjectNode node = (ObjectNode) obj; + Iterator fields = node.fieldNames(); + + while (fields.hasNext()) { + String field = fields.next(); + action.accept(field, node.findValue(field)); + } + } + } + + /** + * Get all properties in an Object. + * @param obj Object to enumerate property names. + * @return enumeration of property names on the Object if it is not a value type. + */ + public static Collection getProperties(Object obj) { + if (obj == null) { + return new ArrayList<>(); + } else if (obj instanceof Map) { + return ((Map) obj).keySet(); + } else if (obj instanceof JsonNode) { + List fields = new ArrayList<>(); + ((JsonNode) obj).fieldNames().forEachRemaining(fields::add); + return fields; + } else { + List fields = new ArrayList<>(); + for (Field field : obj.getClass().getDeclaredFields()) { + fields.add(field.getName()); + } + + return fields; + } + } + + /** + * Detects if property exists on Object. + * @param obj Object. + * @param name name of the property. + * @return true if found. + */ + public static boolean containsProperty(Object obj, String name) { + if (obj == null) { + return false; + } + + if (obj instanceof Map) { + return ((Map) obj).containsKey(name); + } + + if (obj instanceof JsonNode) { + return ((JsonNode) obj).findValue(name) != null; + } + + for (Field field : obj.getClass().getDeclaredFields()) { + if (StringUtils.equalsIgnoreCase(field.getName(), name)) { + return true; + } + } + return false; + } + + /** + * Clone an Object. + * @param Type to clone. + * @param obj The Object. + * @return The Object as Json. + */ + public static T clone(T obj) { + return (T) Serialization.getAs(obj, obj.getClass()); + } + + /** + * Equivalent to javascripts ObjectPath.Assign, creates a new Object from startObject + * overlaying any non-null values from the overlay Object. + * @param The Object type. + * @param startObject Intial Object. + * @param overlayObject Overlay Object. + * @return merged Object. + */ + public static T merge(Object startObject, Object overlayObject) { + return (T) assign(startObject, overlayObject); + } + + /** + * Equivalent to javascripts ObjectPath.Assign, creates a new Object from startObject + * overlaying any non-null values from the overlay Object. + * @param The Object type. + * @param startObject Intial Object. + * @param overlayObject Overlay Object. + * @param type Type of T + * @return merged Object. + */ + public static T merge(Object startObject, Object overlayObject, Class type) { + return (T) assign(startObject, overlayObject, type); + } + + /** + * Equivalent to javascripts ObjectPath.Assign, creates a new Object from startObject + * overlaying any non-null values from the overlay Object. + * @param The target type. + * @param startObject overlay Object of any type. + * @param overlayObject overlay Object of any type. + * @return merged Object. + */ + public static T assign(T startObject, Object overlayObject) { + // FIXME this won't work for null startObject + return (T) assign(startObject, overlayObject, startObject.getClass()); + } + + /** + * Equivalent to javascripts ObjectPath.Assign, creates a new Object from startObject + * overlaying any non-null values from the overlay Object. + * @param The Target type. + * @param startObject intial Object of any type. + * @param overlayObject overlay Object of any type. + * @param type type to output. + * @return merged Object. + */ + public static T assign(Object startObject, Object overlayObject, Class type) { + if (startObject != null && overlayObject != null) { + // make a deep clone JsonNode of the startObject + JsonNode merged = startObject instanceof JsonNode + ? (JsonNode) clone(startObject) + : Serialization.objectToTree(startObject); + + // get a JsonNode of the overlay Object + JsonNode overlay = overlayObject instanceof JsonNode + ? (JsonNode) overlayObject + : Serialization.objectToTree(overlayObject); + + merge(merged, overlay); + + return Serialization.treeToValue(merged, type); + } + + Object singleObject = startObject != null ? startObject : overlayObject; + if (singleObject != null) { + if (singleObject instanceof JsonNode) { + return Serialization.treeToValue((JsonNode) singleObject, type); + } + + return (T) singleObject; + } + + return null; +// TODO default object +// try { +// return singleObject.newInstance(); +// } catch (InstantiationException | IllegalAccessException e) { +// return null; +// } + } + + private static void merge(JsonNode startObject, JsonNode overlayObject) { + Set keySet = mergeKeys(startObject, overlayObject); + + for (String key : keySet) { + JsonNode targetValue = startObject.findValue(key); + JsonNode sourceValue = overlayObject.findValue(key); + + // skip empty overlay items + if (!isNull(sourceValue)) { + if (sourceValue instanceof ObjectNode) { + if (isNull(targetValue)) { + ((ObjectNode) startObject).set(key, clone(sourceValue)); + } else { + merge(targetValue, sourceValue); + } + } else { //if (targetValue instanceof NullNode) { + ((ObjectNode) startObject).set(key, clone(sourceValue)); + } + } + } + } + + private static boolean isNull(JsonNode node) { + return node == null || node instanceof NullNode; + } + + private static Set mergeKeys(JsonNode startObject, JsonNode overlayObject) { + Set keySet = new HashSet<>(); + Iterator> iter = startObject.fields(); + while (iter.hasNext()) { + Entry entry = iter.next(); + keySet.add(entry.getKey()); + } + + iter = overlayObject.fields(); + while (iter.hasNext()) { + Entry entry = iter.next(); + keySet.add(entry.getKey()); + } + + return keySet; + } + + /// + /// Convert a generic Object to a typed Object. + /// + /// type to convert to. + /// value to convert. + /// converted value. + /** + * Convert a generic Object to a typed Object. + * @param type to convert to. + * @param val value to convert. + * @param valueType Type of T + * @return converted value. + */ + public static T mapValueTo(Object val, Class valueType) { + if (val.getClass().equals(valueType)) { + return (T) val; + } + + if (val instanceof JsonNode) { + return Serialization.treeToValue((JsonNode) val, valueType); + } + + return Serialization.getAs(val, valueType); + + /* + if (val instanceof JValue) + { + return ((JValue)val).ToObject(); + } + + if (typeof(T) == typeof(Object)) + { + return (T)val; + } + + if (val is JArray) + { + return ((JArray)val).ToObject(); + } + + if (val is JObject) + { + return ((JObject)val).ToObject(); + } + + if (typeof(T) == typeof(JObject)) + { + return (T)(Object)JObject.FromObject(val); + } + + if (typeof(T) == typeof(JArray)) + { + return (T)(Object)JArray.FromObject(val); + } + + if (typeof(T) == typeof(JValue)) + { + return (T)(Object)JValue.FromObject(val); + } + + if (val is T) + { + return (T)val; + } + + return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(val, _expressionCaseSettings)); + */ + } + + /** + * Given an root Object and property path, resolve to a constant if eval = true or a constant path otherwise. + * conversation[user.name][user.age] to ['conversation', 'joe', 32]. + * @param Type of T + * @param obj root Object. + * @param propertyPath property path to resolve. + * @return True if it was able to resolve all nested references. + */ + public static Segments tryResolvePath(Object obj, String propertyPath) { + return tryResolvePath(obj, propertyPath, false); + } + + /** + * Given an root Object and property path, resolve to a constant if eval = true or a constant path otherwise. + * conversation[user.name][user.age] to ['conversation', 'joe', 32]. + * @param Type of T + * @param obj root Object. + * @param propertyPath property path to resolve. + * @param eval True to evaluate resulting segments. + * @return True if it was able to resolve all nested references. + */ + public static Segments tryResolvePath(Object obj, String propertyPath, boolean eval) { + Segments soFar = new Segments(); + char first = propertyPath.length() > 0 ? propertyPath.charAt(0) : ' '; + if (first == '\'' || first == '"') { + if (!propertyPath.endsWith(String.valueOf(first))) { + return null; + } + + soFar.add(propertyPath.substring(1, propertyPath.length() - 1)); + } else if (isInt(propertyPath)) { + soFar.add(Integer.parseInt(propertyPath)); + } else { + int start = 0; + int i; + + // Scan path evaluating as we go + for (i = 0; i < propertyPath.length(); ++i) { + char ch = propertyPath.charAt(i); + if (ch == '.' || ch == '[') { + // emit + String segment = propertyPath.substring(start, i); + if (!StringUtils.isEmpty(segment)) { + soFar.add(segment); + } + start = i + 1; + } + + if (ch == '[') { + // Bracket expression + int nesting = 1; + while (++i < propertyPath.length()) { + ch = propertyPath.charAt(i); + if (ch == '[') { + ++nesting; + } else if (ch == ']') { + --nesting; + if (nesting == 0) { + break; + } + } + } + + if (nesting > 0) { + // Unbalanced brackets + return null; + } + + String expr = propertyPath.substring(start, i); + start = i + 1; + Segments indexer = tryResolvePath(obj, expr, true); + if (indexer == null || indexer.size() != 1) { + // Could not resolve bracket expression + return null; + } + + String result = mapValueTo(indexer.first(), String.class); + if (isInt(result)) { + soFar.add(Integer.parseInt(result)); + } else { + soFar.add(result); + } + } + } + + // emit + String segment = propertyPath.substring(start, i); + if (!StringUtils.isEmpty(segment)) { + soFar.add(segment); + } + start = i + 1; + + if (eval) { + Object result = resolveSegments(obj, soFar); + if (result == null) { + return null; + } + + soFar.clear(); + soFar.add(mapValueTo(result, Object.class)); + } + } + + return soFar; + } + + private static Object resolveSegment(Object current, Object segment) { + if (current != null) { + if (segment instanceof Integer) { + int index = (Integer) segment; + + if (current instanceof List) { + current = ((List) current).get(index); + } else if (current instanceof ArrayNode) { + current = ((ArrayNode) current).get(index); + } else { + current = Array.get(current, index); + } + } else { + current = getObjectProperty(current, (String) segment); + } + } + + return current; + } + + private static Object resolveSegments(Object current, Segments segments) { + Object result = current; + for (Object segment : segments) { + result = resolveSegment(result, segment); + if (result == null) { + return null; + } + } + + return result; + } + + /// + /// Get a property or array element from an Object. + /// + /// Object. + /// property or array segment to get relative to the Object. + /// the value or null if not found. + private static Object getObjectProperty(Object obj, String property) { + if (obj == null) { + return null; + } + + // Because TurnContextStateCollection is not implemented as a Map we need to + // set obj to the Map which holds the state values which is retrieved from calling + // getTurnStateServices() + if (obj instanceof TurnContextStateCollection) { + Map dict = ((TurnContextStateCollection) obj).getTurnStateServices(); + List> matches = dict.entrySet().stream() + .filter(key -> key.getKey().equalsIgnoreCase(property)) + .collect(Collectors.toList()); + + if (matches.size() > 0) { + return matches.get(0).getValue(); + } + + return null; + } + + if (obj instanceof Map) { + Map dict = (Map) obj; + List> matches = dict.entrySet().stream() + .filter(key -> key.getKey().equalsIgnoreCase(property)) + .collect(Collectors.toList()); + + if (matches.size() > 0) { + return matches.get(0).getValue(); + } + + return null; + } + + if (obj instanceof JsonNode) { + JsonNode node = (JsonNode) obj; + Iterator fields = node.fieldNames(); + while (fields.hasNext()) { + String field = fields.next(); + if (field.equalsIgnoreCase(property)) { + return node.findValue(field); + } + } + return null; + } + + /* + //!!! not sure Java equiv + if (obj is JValue jval) + { + // in order to make things like "this.value.Length" work, when "this.value" is a String. + return getObjectProperty(jval.Value, property); + } + */ + + // reflection on Object + List matches = Arrays.stream(obj.getClass().getDeclaredFields()) + .filter(field -> field.getName().equalsIgnoreCase(property)) + .map(field -> { + try { + return field.get(obj); + } catch (IllegalAccessException e) { + return null; + } + }) + .collect(Collectors.toList()); + + if (matches.size() > 0) { + return matches.get(0); + } + return null; + } + + /// + /// Given an Object, set a property or array element on it with a value. + /// + /// Object to modify. + /// property or array segment to put the value in. + /// value to store. + /// if true, value will be normalized to JSON primitive Objects. + @SuppressWarnings("PMD.UnusedFormalParameter") + private static void setObjectSegment(Object obj, Object segment, Object value) { + setObjectSegment(obj, segment, value, true); + } + + @SuppressWarnings("PMD.EmptyCatchBlock") + private static void setObjectSegment(Object obj, Object segment, Object value, boolean json) { + Object normalizedValue = getNormalizedValue(value, json); + + // Json Array + if (segment instanceof Integer) { + ArrayNode jar = (ArrayNode) obj; + int index = (Integer) segment; + + if (index >= jar.size()) { + jar.add(index + 1 - jar.size()); + } + + jar.set(index, Serialization.objectToTree(normalizedValue)); + return; + } + + // Map + String property = (String) segment; + if (obj instanceof Map) { + Boolean wasSet = false; + Map dict = (Map) obj; + for (String key : dict.keySet()) { + if (key.equalsIgnoreCase(property)) { + wasSet = true; + dict.put(key, normalizedValue); + break; + } + } + if (!wasSet) { + dict.put(property, normalizedValue); + } + + return; + } + + // ObjectNode + if (obj instanceof ObjectNode) { + boolean wasSet = false; + ObjectNode node = (ObjectNode) obj; + Iterator fields = node.fieldNames(); + while (fields.hasNext()) { + String field = fields.next(); + if (field.equalsIgnoreCase(property)) { + wasSet = true; + node.set(property, Serialization.objectToTree(normalizedValue)); + break; + } + } + if (!wasSet) { + node.set(property, Serialization.objectToTree(normalizedValue)); + } + + return; + } + + // reflection + if (obj != null) { + for (Field f : obj.getClass().getDeclaredFields()) { + if (f.getName().equalsIgnoreCase(property)) { + try { + f.set(obj, normalizedValue); + } catch (IllegalAccessException ignore) { + } + } + } + } + } + + /// + /// Normalize value as json Objects. + /// + /// value to normalize. + /// normalize as json Objects. + /// normalized value. + private static Object getNormalizedValue(Object value, boolean json) { + Object val; + + if (json) { + //TODO revisit this (from dotnet) + if (value instanceof JsonNode) { + val = clone(value); + } else if (value == null) { + val = null; + } else { + val = clone(value); + } + } else { + val = value; + } + + return val; + } + + private static boolean isInt(String value) { + try { + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/PersistedState.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/PersistedState.java new file mode 100644 index 000000000..e2e7bf03f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/PersistedState.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.HashMap; + +/** + * Represents the persisted data across turns. + */ +public class PersistedState { + + /** + * The user state. + */ + private HashMap userState; + + /** + * The converation state. + */ + private HashMap conversationState; + + /** + * Constructs a PersistedState object. + */ + public PersistedState() { + userState = new HashMap(); + conversationState = new HashMap(); + } + + /** + * Initializes a new instance of the PersistedState class. + * + * @param keys The persisted keys. + * @param data The data containing the state values. + */ + @SuppressWarnings("unchecked") + public PersistedState(PersistedStateKeys keys, HashMap data) { + if (data.containsKey(keys.getUserState())) { + userState = (HashMap) data.get(keys.getUserState()); + } + if (data.containsKey(keys.getConversationState())) { + userState = (HashMap) data.get(keys.getConversationState()); + } + } + + /** + * @return userState Gets the user profile data. + */ + public HashMap getUserState() { + return this.userState; + } + + /** + * @param withUserState Sets user profile data. + */ + public void setUserState(HashMap withUserState) { + this.userState = withUserState; + } + + /** + * @return conversationState Gets the dialog state data. + */ + public HashMap getConversationState() { + return this.conversationState; + } + + /** + * @param withConversationState Sets the dialog state data. + */ + public void setConversationState(HashMap withConversationState) { + this.conversationState = withConversationState; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/PersistedStateKeys.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/PersistedStateKeys.java new file mode 100644 index 000000000..17c10440a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/PersistedStateKeys.java @@ -0,0 +1,48 @@ +// Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. All rights reserved. + +package com.microsoft.bot.dialogs; + +/** + * These are the keys which are persisted. + */ +public class PersistedStateKeys { + /** + * The key for the user state. + */ + private String userState; + + /** + * The conversation state. + */ + private String conversationState; + + /** + * @return Gets the user state; + */ + public String getUserState() { + return this.userState; + } + + /** + * @param withUserState Sets the user state. + */ + public void setUserState(String withUserState) { + this.userState = withUserState; + } + + /** + * @return Gets the conversation state. + */ + public String getConversationState() { + return this.conversationState; + } + + /** + * @param withConversationState Sets the conversation state. + */ + public void setConversationState(String withConversationState) { + this.conversationState = withConversationState; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Recognizer.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Recognizer.java new file mode 100644 index 000000000..411081038 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Recognizer.java @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.IntentScore; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.RecognizerConvert; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Serialization; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Recognizer base class. + * + *

Recognizers operate in a DialogContext environment to recognize user input into Intents + * and Entities.

+ * + *

+ * This class models 3 virtual methods around + * * Pure DialogContext (where the recognition happens against current state dialogcontext + * * Activity (where the recognition is from an Activity) + * * Text/Locale (where the recognition is from text/locale) + *

+ * + *

+ * The default implementation of DialogContext method is to use Context.Activity and call the + * activity method. + * The default implementation of Activity method is to filter to Message activities and pull + * out text/locale and call the text/locale method. + *

+ */ +public class Recognizer { + /** + * Intent name that will be produced by this recognizer if the child recognizers do not + * have consensus for intents. + */ + public static final String CHOOSE_INTENT = "ChooseIntent"; + + /** + * Standard none intent that means none of the recognizers recognize the intent. If each + * recognizer returns no intents or None intents, then this recognizer will return None intent. + */ + public static final String NONE_INTENT = "None"; + + @JsonProperty(value = "id") + private String id; + + @JsonIgnore + private BotTelemetryClient telemetryClient = new NullBotTelemetryClient(); + + /** + * Initializes a new Recognizer. + */ + public Recognizer() { + } + + /** + * Runs current DialogContext.TurnContext.Activity through a recognizer and returns a + * generic recognizer result. + * + * @param dialogContext Dialog Context. + * @param activity activity to recognize. + * @return Analysis of utterance. + */ + public CompletableFuture recognize( + DialogContext dialogContext, + Activity activity + ) { + return recognize(dialogContext, activity, null, null); + } + + /** + * Runs current DialogContext.TurnContext.Activity through a recognizer and returns a + * generic recognizer result. + * + * @param dialogContext Dialog Context. + * @param activity activity to recognize. + * @param telemetryProperties The properties to be included as part of the event tracking. + * @param telemetryMetrics The metrics to be included as part of the event tracking. + * @return Analysis of utterance. + */ + public CompletableFuture recognize( + DialogContext dialogContext, + Activity activity, + Map telemetryProperties, + Map telemetryMetrics + ) { + return Async.completeExceptionally(new NotImplementedException("recognize")); + } + + /** + * Runs current DialogContext.TurnContext.Activity through a recognizer and returns a + * strongly-typed recognizer result using RecognizerConvert. + * + * @param dialogContext Dialog Context. + * @param activity activity to recognize. + * @param telemetryProperties The properties to be included as part of the event tracking. + * @param telemetryMetrics The metrics to be included as part of the event tracking. + * @param c Class of type T. + * @param The RecognizerConvert. + * @return Analysis of utterance. + */ + public CompletableFuture recognize( + DialogContext dialogContext, + Activity activity, + Map telemetryProperties, + Map telemetryMetrics, + Class c + ) { + return Async.tryCompletable(() -> { + T result = c.newInstance(); + return recognize(dialogContext, activity, telemetryProperties, telemetryMetrics) + .thenApply(recognizerResult -> { + result.convert(recognizerResult); + return result; + }); + }); + } + + /** + * Returns ChooseIntent between multiple recognizer results. + * + * @param recognizerResults recognizer Id to recognizer results map. + * @return recognizerResult which is ChooseIntent. + */ + protected static RecognizerResult createChooseIntentResult(Map recognizerResults) { + String text = null; + List candidates = new ArrayList<>(); + + for (Map.Entry recognizerResult : recognizerResults.entrySet()) { + text = recognizerResult.getValue().getText(); + RecognizerResult.NamedIntentScore top = recognizerResult.getValue().getTopScoringIntent(); + if (!StringUtils.equals(top.intent, NONE_INTENT)) { + ObjectNode candidate = Serialization.createObjectNode(); + candidate.put("id", recognizerResult.getKey()); + candidate.put("intent", top.intent); + candidate.put("score", top.score); + candidate.put("result", Serialization.objectToTree(recognizerResult.getValue())); + candidates.add(candidate); + } + } + + RecognizerResult result = new RecognizerResult(); + Map intents = new HashMap<>(); + + if (!candidates.isEmpty()) { + // return ChooseIntent with candidates array + IntentScore intent = new IntentScore(); + intent.setScore(1.0); + intents.put(CHOOSE_INTENT, intent); + + result.setText(text); + result.setIntents(intents); + result.setProperties("candidates", Serialization.objectToTree(candidates)); + } else { + // just return a none intent + IntentScore intent = new IntentScore(); + intent.setScore(1.0); + intents.put(NONE_INTENT, intent); + + result.setText(text); + result.setIntents(intents); + } + + return result; + } + + /** + * Gets id of the recognizer. + * @return id of the recognizer + */ + public String getId() { + return id; + } + + /** + * Sets id of the recognizer. + * @param withId id of the recognizer + */ + public void setId(String withId) { + id = withId; + } + + /** + * Gets the currently configured BotTelemetryClient that logs the RecognizerResult event. + * @return BotTelemetryClient + */ + public BotTelemetryClient getTelemetryClient() { + return telemetryClient; + } + + /** + * Sets the currently configured BotTelemetryClient that logs the RecognizerResult event. + * @param withTelemetryClient BotTelemetryClient + */ + public void setTelemetryClient(BotTelemetryClient withTelemetryClient) { + telemetryClient = withTelemetryClient; + } + + /** + * Uses the RecognizerResult to create a list of propeties to be included when tracking the + * result in telemetry. + * + * @param recognizerResult Recognizer Result. + * @param telemetryProperties A list of properties to append or override the properties + * created using the RecognizerResult. + * @param dialogContext Dialog Context. + * @return A dictionary that can be included when calling the TrackEvent method on the + * TelemetryClient. + */ + protected Map fillRecognizerResultTelemetryProperties( + RecognizerResult recognizerResult, + Map telemetryProperties, + DialogContext dialogContext + ) { + Map properties = new HashMap<>(); + properties.put("Text", recognizerResult.getText()); + properties.put("AlteredText", recognizerResult.getAlteredText()); + properties.put("TopIntent", !recognizerResult.getIntents().isEmpty() + ? recognizerResult.getTopScoringIntent().intent : null); + properties.put("TopIntentScore", !recognizerResult.getIntents().isEmpty() + ? Double.toString(recognizerResult.getTopScoringIntent().score) : null); + properties.put("Intents", !recognizerResult.getIntents().isEmpty() + ? Serialization.toStringSilent(recognizerResult.getIntents()) : null); + properties.put("Entities", recognizerResult.getEntities() != null + ? Serialization.toStringSilent(recognizerResult.getEntities()) : null); + properties.put("AdditionalProperties", !recognizerResult.getProperties().isEmpty() + ? Serialization.toStringSilent(recognizerResult.getProperties()) : null); + + // Additional Properties can override "stock" properties. + if (telemetryProperties != null) { + properties.putAll(telemetryProperties); + } + + return properties; + } + + /** + * Tracks an event with the event name provided using the TelemetryClient attaching the + * properties / metrics. + * + * @param dialogContext Dialog Context. + * @param eventName The name of the event to track. + * @param telemetryProperties The properties to be included as part of the event tracking. + * @param telemetryMetrics The metrics to be included as part of the event tracking. + */ + protected void trackRecognizerResult( + DialogContext dialogContext, + String eventName, + Map telemetryProperties, + Map telemetryMetrics + ) { + if (telemetryClient instanceof NullBotTelemetryClient) { + BotTelemetryClient turnStateTelemetryClient = dialogContext.getContext() + .getTurnState().get(BotTelemetryClient.class); + telemetryClient = turnStateTelemetryClient != null ? turnStateTelemetryClient : telemetryClient; + } + + telemetryClient.trackEvent(eventName, telemetryProperties, telemetryMetrics); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ScopePath.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ScopePath.java new file mode 100644 index 000000000..5ad4a54bb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ScopePath.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Defines paths for the available scopes. + */ +public final class ScopePath { + + private ScopePath() { + } + + /** + * User memory scope root path. + */ + public static final String USER = "user"; + + /** + * Conversation memory scope root path. + */ + public static final String CONVERSATION = "conversation"; + + /** + * Dialog memory scope root path. + */ + public static final String DIALOG = "dialog"; + + /** + * DialogClass memory scope root path. + */ + public static final String DIALOG_CLASS = "dialogclass"; + + /** + * DialogContext memory scope root path. + */ + public static final String DIALOG_CONTEXT = "dialogContext"; + + /** + * This memory scope root path. + */ + public static final String THIS = "this"; + + /** + * Class memory scope root path. + */ + public static final String CLASS = "class"; + + /** + * Settings memory scope root path. + */ + public static final String SETTINGS = "settings"; + + /** + * Turn memory scope root path. + */ + public static final String TURN = "turn"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SegmentType.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SegmentType.java new file mode 100644 index 000000000..b3e044a43 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SegmentType.java @@ -0,0 +1,33 @@ +package com.microsoft.bot.dialogs; + +/** + * A class wraps an Object and can assist in determining if it's an integer. + */ +@SuppressWarnings("checkstyle:VisibilityModifier") +class SegmentType { + + public boolean isInt; + public int intValue; + public Segments segmentsValue; + public String stringValue; + + /** + * @param value The object to create a SegmentType for. + */ + SegmentType(Object value) { + try { + intValue = Integer.parseInt((String) value); + isInt = true; + } catch (NumberFormatException e) { + isInt = false; + } + + if (!isInt) { + if (value instanceof Segments) { + segmentsValue = (Segments) value; + } else { + stringValue = (String) value; + } + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Segments.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Segments.java new file mode 100644 index 000000000..e2ea8779b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/Segments.java @@ -0,0 +1,37 @@ +package com.microsoft.bot.dialogs; + +import java.util.ArrayList; + +/** + * Generic Arraylist of Object. + */ +class Segments extends ArrayList { + + /** + * Returns the first item in the collection. + * + * @return the first object. + */ + public Object first() { + return get(0); + } + + /** + * Returns the last item in the collection. + * + * @return the last object. + */ + public Object last() { + return get(size() - 1); + } + + /** + * Gets the SegmentType at the specified index. + * + * @param index Index of the requested segment. + * @return The SegmentType of item at the requested index. + */ + public SegmentType getSegment(int index) { + return new SegmentType(get(index)); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialog.java new file mode 100644 index 000000000..ab7df729c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialog.java @@ -0,0 +1,521 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.dialogs; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserTokenProvider; +import com.microsoft.bot.builder.skills.BotFrameworkSkill; +import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.ExpectedReplies; +import com.microsoft.bot.schema.OAuthCard; +import com.microsoft.bot.schema.SignInConstants; +import com.microsoft.bot.schema.TokenExchangeInvokeRequest; +import com.microsoft.bot.schema.TokenExchangeRequest; + +import org.apache.commons.lang3.StringUtils; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * A specialized {@link Dialog} that can wrap remote calls to a skill. + * + * The options parameter in BeginDialog must be a + * {@link BeginSkillDialogOptions} instancewith the initial parameters for the + * dialog. + */ +public class SkillDialog extends Dialog { + + private SkillDialogOptions dialogOptions; + + private final String deliverModeStateKey = "deliverymode"; + private final String skillConversationIdStateKey = "Microsoft.Bot.Builder.Dialogs.SkillDialog.SkillConversationId"; + + /** + * Initializes a new instance of the {@link SkillDialog} class to wrap remote + * calls to a skill. + * + * @param dialogOptions The options to execute the skill dialog. + * @param dialogId The id of the dialog. + */ + public SkillDialog(SkillDialogOptions dialogOptions, String dialogId) { + super(dialogId); + if (dialogOptions == null) { + throw new IllegalArgumentException("dialogOptions cannot be null."); + } + + this.dialogOptions = dialogOptions; + } + + /** + * Called when the skill dialog is started and pushed onto the dialog stack. + * + * @param dc The {@link DialogContext} for the current turn of + * conversation. + * @param options Optional, initial information to pass to the dialog. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + @Override + public CompletableFuture beginDialog(DialogContext dc, Object options) { + BeginSkillDialogOptions dialogArgs = validateBeginDialogArgs(options); + + // Create deep clone of the original activity to avoid altering it before + // forwarding it. + Activity skillActivity = Activity.clone(dialogArgs.getActivity()); + + // Apply conversation reference and common properties from incoming activity + // before sending. + ConversationReference conversationReference = dc.getContext().getActivity().getConversationReference(); + skillActivity.applyConversationReference(conversationReference, true); + + // Store delivery mode and connection name in dialog state for later use. + dc.getActiveDialog().getState().put(deliverModeStateKey, dialogArgs.getActivity().getDeliveryMode()); + + // Create the conversationId and store it in the dialog context state so we can + // use it later + return createSkillConversationId(dc.getContext(), dc.getContext().getActivity()) + .thenCompose(skillConversationId -> { + dc.getActiveDialog().getState().put(skillConversationIdStateKey, skillConversationId); + + // Send the activity to the skill. + return sendToSkill(dc.getContext(), skillActivity, skillConversationId).thenCompose(eocActivity -> { + if (eocActivity != null) { + return dc.endDialog(eocActivity.getValue()); + } + return CompletableFuture.completedFuture(END_OF_TURN); + }); + }); + } + + /** + * Called when the skill dialog is _continued_, where it is the active dialog + * and the user replies with a new activity. + * + * @param dc The {@link DialogContext} for the current turn of conversation. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * result may also contain a return value. + */ + @Override + public CompletableFuture continueDialog(DialogContext dc) { + + Boolean interrupted = dc.getState().getValue(TurnPath.INTERRUPTED, false, Boolean.class); + if (interrupted) { + dc.getState().setValue(TurnPath.INTERRUPTED, false); + return resumeDialog(dc, DialogReason.END_CALLED); + } + + + if (!onValidateActivity(dc.getContext().getActivity())) { + return CompletableFuture.completedFuture(END_OF_TURN); + } + + // Handle EndOfConversation from the skill (this will be sent to the this dialog + // by the SkillHandler + // if received from the Skill) + if (dc.getContext().getActivity().getType().equals(ActivityTypes.END_OF_CONVERSATION)) { + return dc.endDialog(dc.getContext().getActivity().getValue()); + } + + // Create deep clone of the original activity to avoid altering it before + // forwarding it. + Activity skillActivity = Activity.clone(dc.getContext().getActivity()); + if (dc.getActiveDialog().getState().get(deliverModeStateKey) != null) { + skillActivity.setDeliveryMode((String) dc.getActiveDialog().getState().get(deliverModeStateKey)); + } + + String skillConversationId = (String) dc.getActiveDialog().getState().get(skillConversationIdStateKey); + + // Just forward to the remote skill + return sendToSkill(dc.getContext(), skillActivity, skillConversationId).thenCompose(eocActivity -> { + if (eocActivity != null) { + return dc.endDialog(eocActivity.getValue()); + } + + return CompletableFuture.completedFuture(END_OF_TURN); + }); + } + + /** + * Called when the skill dialog should re-prompt the user for input. + * + * @param turnContext The context Object for this turn. + * @param instance State information for this dialog. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { + // Create and send an envent to the skill so it can resume the dialog. + Activity repromptEvent = Activity.createEventActivity(); + repromptEvent.setName(DialogEvents.REPROMPT_DIALOG); + + // Apply conversation reference and common properties from incoming activity + // before sending. + repromptEvent.applyConversationReference(turnContext.getActivity().getConversationReference(), true); + + String skillConversationId = (String) instance.getState().get(skillConversationIdStateKey); + + // connection Name instanceof not applicable for a RePrompt, as we don't expect + // as OAuthCard in response. + return sendToSkill(turnContext, (Activity) repromptEvent, skillConversationId).thenApply(result -> null); + } + + /** + * Called when a child skill dialog completed its turn, returning control to + * this dialog. + * + * @param dc The dialog context for the current turn of the conversation. + * @param reason Reason why the dialog resumed. + * @param result Optional, value returned from the dialog that was called. The + * type of the value returned is dependent on the child dialog. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + return repromptDialog(dc.getContext(), dc.getActiveDialog()).thenCompose(x -> { + return CompletableFuture.completedFuture(END_OF_TURN); + }); + } + + /** + * Called when the skill dialog is ending. + * + * @param turnContext The context Object for this turn. + * @param instance State information associated with the instance of this + * dialog on the dialog stack. + * @param reason Reason why the dialog ended. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + // Send of of conversation to the skill if the dialog has been cancelled. + return onEndDialog(turnContext, instance, reason) + .thenCompose(result -> super.endDialog(turnContext, instance, reason)); + } + + private CompletableFuture onEndDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + if (reason == DialogReason.CANCEL_CALLED || reason == DialogReason.REPLACE_CALLED) { + Activity activity = Activity.createEndOfConversationActivity(); + + // Apply conversation reference and common properties from incoming activity + // before sending. + activity.applyConversationReference(turnContext.getActivity().getConversationReference(), true); + activity.setChannelData(turnContext.getActivity().getChannelData()); + for (Map.Entry entry : turnContext.getActivity().getProperties().entrySet()) { + activity.setProperties(entry.getKey(), entry.getValue()); + } + + String skillConversationId = (String) instance.getState().get(skillConversationIdStateKey); + + // connection Name instanceof not applicable for an EndDialog, as we don't + // expect as OAuthCard in response. + return sendToSkill(turnContext, activity, skillConversationId).thenApply(result -> null); + } else { + return CompletableFuture.completedFuture(null); + } + + } + + /** + * Validates the activity sent during ContinueDialog . + * + * @param activity The {@link Activity} for the current turn of conversation. + * + * Override this method to implement a custom validator for the + * activity being sent during the ContinueDialog . This + * method can be used to ignore activities of a certain type if + * needed. If this method returns false, the dialog will end the + * turn without processing the activity. + * + * @return true if the activity is valid, false if not. + */ + protected boolean onValidateActivity(Activity activity) { + return true; + } + + /** + * Validates the required properties are set in the options argument passed to + * the BeginDialog call. + */ + private static BeginSkillDialogOptions validateBeginDialogArgs(Object options) { + if (options == null) { + throw new IllegalArgumentException("options cannot be null."); + } + + if (!(options instanceof BeginSkillDialogOptions)) { + throw new IllegalArgumentException("Unable to cast options to beginSkillDialogOptions}"); + } + + BeginSkillDialogOptions dialogArgs = (BeginSkillDialogOptions) options; + + if (dialogArgs.getActivity() == null) { + throw new IllegalArgumentException("dialogArgs.getActivity is null in options"); + } + + return dialogArgs; + } + + private CompletableFuture sendToSkill(TurnContext context, Activity activity, + String skillConversationId) { + if (activity.getType().equals(ActivityTypes.INVOKE)) { + // Force ExpectReplies for invoke activities so we can get the replies right + // away and send them + // back to the channel if needed. This makes sure that the dialog will receive + // the Invoke response + // from the skill and any other activities sent, including EoC. + activity.setDeliveryMode(DeliveryModes.EXPECT_REPLIES.toString()); + } + + // Always save state before forwarding + // (the dialog stack won't get updated with the skillDialog and things won't + // work if you don't) + getDialogOptions().getConversationState().saveChanges(context, true); + + BotFrameworkSkill skillInfo = getDialogOptions().getSkill(); + return getDialogOptions().getSkillClient() + .postActivity(getDialogOptions().getBotId(), skillInfo.getAppId(), skillInfo.getSkillEndpoint(), + getDialogOptions().getSkillHostEndpoint(), skillConversationId, activity, Object.class) + .thenCompose(response -> { + // Inspect the skill response status + if (!response.getIsSuccessStatusCode()) { + return Async.completeExceptionally(new SkillInvokeException(String.format( + "Error invoking the skill id: %s at %s (status is %s). %s", skillInfo.getId(), + skillInfo.getSkillEndpoint(), response.getStatus(), response.getBody()))); + } + + ExpectedReplies replies = null; + if (response.getBody() instanceof ExpectedReplies) { + replies = (ExpectedReplies) response.getBody(); + } + + Activity eocActivity = null; + if (activity.getDeliveryMode() != null + && activity.getDeliveryMode().equals(DeliveryModes.EXPECT_REPLIES.toString()) + && replies.getActivities() != null && replies.getActivities().size() > 0) { + // Track sent invoke responses, so more than one instanceof not sent. + boolean sentInvokeResponse = false; + + // Process replies in the response.getBody(). + for (Activity activityFromSkill : replies.getActivities()) { + if (activityFromSkill.getType().equals(ActivityTypes.END_OF_CONVERSATION)) { + // Capture the EndOfConversation activity if it was sent from skill + eocActivity = activityFromSkill; + + // The conversation has ended, so cleanup the conversation id. + getDialogOptions().getConversationIdFactory() + .deleteConversationReference(skillConversationId).join(); + } else if (!sentInvokeResponse && interceptOAuthCards(context, activityFromSkill, + getDialogOptions().getConnectionName()).join()) { + // do nothing. Token exchange succeeded, so no OAuthCard needs to be shown to + // the user + sentInvokeResponse = true; + } else { + if (activityFromSkill.getType().equals(ActivityTypes.INVOKE_RESPONSE)) { + // An invoke respones has already been sent. This instanceof a bug in the skill. + // Multiple invoke responses are not possible. + if (sentInvokeResponse) { + continue; + } + + sentInvokeResponse = true; + + // Not sure this is needed in Java, looks like a workaround for some .NET issues + // Ensure the value in the invoke response instanceof of type InvokeResponse + // (it gets deserialized as JObject by default). + + // if (activityFromSkill.getValue() instanceof JObject jObject) { + // activityFromSkill.setValue(jObject.ToObject()); + // } + } + + // Send the response back to the channel. + context.sendActivity(activityFromSkill); + } + } + } + + return CompletableFuture.completedFuture(eocActivity); + + }); + } + + /** + * Tells is if we should intercept the OAuthCard message. + * + * The SkillDialog only attempts to intercept OAuthCards when the following + * criteria are met: 1. An OAuthCard was sent from the skill 2. The SkillDialog + * was called with a connectionName 3. The current adapter supports token + * exchange If any of these criteria are false, return false. + */ + private CompletableFuture interceptOAuthCards(TurnContext turnContext, Activity activity, + String connectionName) { + + UserTokenProvider tokenExchangeProvider; + + if (StringUtils.isEmpty(connectionName) || !(turnContext.getAdapter() instanceof UserTokenProvider)) { + // The adapter may choose not to support token exchange, + // in which case we fallback to showing an oauth card to the user. + return CompletableFuture.completedFuture(false); + } else { + tokenExchangeProvider = (UserTokenProvider) turnContext.getAdapter(); + } + + Attachment oauthCardAttachment = null; + + if (activity.getAttachments() != null) { + Optional optionalAttachment = activity.getAttachments().stream() + .filter(a -> a.getContentType() != null && a.getContentType().equals(OAuthCard.CONTENTTYPE)) + .findFirst(); + if (optionalAttachment.isPresent()) { + oauthCardAttachment = optionalAttachment.get(); + } + } + + if (oauthCardAttachment != null) { + OAuthCard oauthCard = (OAuthCard) oauthCardAttachment.getContent(); + if (oauthCard != null && oauthCard.getTokenExchangeResource() != null + && !StringUtils.isEmpty(oauthCard.getTokenExchangeResource().getUri())) { + try { + return tokenExchangeProvider + .exchangeToken(turnContext, connectionName, turnContext.getActivity().getFrom().getId(), + new TokenExchangeRequest(oauthCard.getTokenExchangeResource().getUri(), null)) + .thenCompose(result -> { + if (result != null && !StringUtils.isEmpty(result.getToken())) { + // If token above instanceof null, then SSO has failed and hence we return + // false. + // If not, send an invoke to the skill with the token. + return sendTokenExchangeInvokeToSkill(activity, + oauthCard.getTokenExchangeResource().getId(), oauthCard.getConnectionName(), + result.getToken()); + } else { + return CompletableFuture.completedFuture(false); + } + + }); + } catch (Exception ex) { + // Failures in token exchange are not fatal. They simply mean that the user + // needs + // to be shown the OAuth card. + return CompletableFuture.completedFuture(false); + } + } + } + return CompletableFuture.completedFuture(false); + } + + // private CompletableFuture interceptOAuthCards(TurnContext turnContext, Activity activity, + // String connectionName) { + + // UserTokenProvider tokenExchangeProvider; + + // if (StringUtils.isEmpty(connectionName) || !(turnContext.getAdapter() instanceof UserTokenProvider)) { + // // The adapter may choose not to support token exchange, + // // in which case we fallback to showing an oauth card to the user. + // return CompletableFuture.completedFuture(false); + // } else { + // tokenExchangeProvider = (UserTokenProvider) turnContext.getAdapter(); + // } + + // Attachment oauthCardAttachment = null; + + // if (activity.getAttachments() != null) { + // Optional optionalAttachment = activity.getAttachments().stream() + // .filter(a -> a.getContentType() != null && a.getContentType().equals(OAuthCard.CONTENTTYPE)) + // .findFirst(); + // if (optionalAttachment.isPresent()) { + // oauthCardAttachment = optionalAttachment.get(); + // } + // } + + // if (oauthCardAttachment != null) { + // OAuthCard oauthCard = (OAuthCard) oauthCardAttachment.getContent(); + // if (oauthCard != null && oauthCard.getTokenExchangeResource() != null + // && !StringUtils.isEmpty(oauthCard.getTokenExchangeResource().getUri())) { + // try { + // TokenResponse result = tokenExchangeProvider + // .exchangeToken(turnContext, connectionName, turnContext.getActivity().getFrom().getId(), + // new TokenExchangeRequest(oauthCard.getTokenExchangeResource().getUri(), null)) + // .join(); + + // if (result != null && !StringUtils.isEmpty(result.getToken())) { + // // If token above instanceof null, then SSO has failed and hence we return + // // false. + // // If not, send an invoke to the skill with the token. + // return sendTokenExchangeInvokeToSkill(activity, oauthCard.getTokenExchangeResource().getId(), + // oauthCard.getConnectionName(), result.getToken()); + // } + // } catch (Exception ex) { + // // Failures in token exchange are not fatal. They simply mean that the user + // // needs + // // to be shown the OAuth card. + // return CompletableFuture.completedFuture(false); + // } + // } + // } + + // return CompletableFuture.completedFuture(false); + // } + + + private CompletableFuture sendTokenExchangeInvokeToSkill(Activity incomingActivity, String id, + String connectionName, String token) { + Activity activity = incomingActivity.createReply(); + activity.setType(ActivityTypes.INVOKE); + activity.setName(SignInConstants.TOKEN_EXCHANGE_OPERATION_NAME); + TokenExchangeInvokeRequest tokenRequest = new TokenExchangeInvokeRequest(); + tokenRequest.setId(id); + tokenRequest.setToken(token); + tokenRequest.setConnectionName(connectionName); + activity.setValue(tokenRequest); + + // route the activity to the skill + BotFrameworkSkill skillInfo = getDialogOptions().getSkill(); + return getDialogOptions().getSkillClient() + .postActivity(getDialogOptions().getBotId(), skillInfo.getAppId(), skillInfo.getSkillEndpoint(), + getDialogOptions().getSkillHostEndpoint(), incomingActivity.getConversation().getId(), activity, + Object.class) + .thenApply(response -> response.getIsSuccessStatusCode()); + } + + private CompletableFuture createSkillConversationId(TurnContext context, Activity activity) { + // Create a conversationId to interact with the skill and send the activity + SkillConversationIdFactoryOptions conversationIdFactoryOptions = new SkillConversationIdFactoryOptions(); + conversationIdFactoryOptions.setFromBotOAuthScope(context.getTurnState().get(BotAdapter.OAUTH_SCOPE_KEY)); + conversationIdFactoryOptions.setFromBotId(getDialogOptions().getBotId()); + conversationIdFactoryOptions.setActivity(activity); + conversationIdFactoryOptions.setBotFrameworkSkill(getDialogOptions().getSkill()); + + return getDialogOptions().getConversationIdFactory().createSkillConversationId(conversationIdFactoryOptions); + } + + /** + * Gets the options used to execute the skill dialog. + * + * @return the DialogOptions value as a SkillDialogOptions. + */ + protected SkillDialogOptions getDialogOptions() { + return this.dialogOptions; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java new file mode 100644 index 000000000..53eb9dac1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.net.URI; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.skills.BotFrameworkClient; +import com.microsoft.bot.builder.skills.BotFrameworkSkill; +import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase; + +/** + * Defines the options that will be used to execute a {@link SkillDialog} . + */ +public class SkillDialogOptions { + + @JsonProperty(value = "botId") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String botId; + + private BotFrameworkClient skillClient; + + @JsonProperty(value = "skillHostEndpoint") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private URI skillHostEndpoint; + + @JsonProperty(value = "skill") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private BotFrameworkSkill skill; + + private SkillConversationIdFactoryBase conversationIdFactory; + + private ConversationState conversationState; + + @JsonProperty(value = "connectionName") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String connectionName; + + /** + * Gets the Microsoft app ID of the bot calling the skill. + * @return the BotId value as a String. + */ + public String getBotId() { + return this.botId; + } + + /** + * Sets the Microsoft app ID of the bot calling the skill. + * @param withBotId The BotId value. + */ + public void setBotId(String withBotId) { + this.botId = withBotId; + } + /** + * Gets the {@link BotFrameworkClient} used to call the remote + * skill. + * @return the SkillClient value as a BotFrameworkClient. + */ + public BotFrameworkClient getSkillClient() { + return this.skillClient; + } + + /** + * Sets the {@link BotFrameworkClient} used to call the remote + * skill. + * @param withSkillClient The SkillClient value. + */ + public void setSkillClient(BotFrameworkClient withSkillClient) { + this.skillClient = withSkillClient; + } + /** + * Gets the callback Url for the skill host. + * @return the SkillHostEndpoint value as a Uri. + */ + public URI getSkillHostEndpoint() { + return this.skillHostEndpoint; + } + + /** + * Sets the callback Url for the skill host. + * @param withSkillHostEndpoint The SkillHostEndpoint value. + */ + public void setSkillHostEndpoint(URI withSkillHostEndpoint) { + this.skillHostEndpoint = withSkillHostEndpoint; + } + /** + * Gets the {@link BotFrameworkSkill} that the dialog will call. + * @return the Skill value as a BotFrameworkSkill. + */ + public BotFrameworkSkill getSkill() { + return this.skill; + } + + /** + * Sets the {@link BotFrameworkSkill} that the dialog will call. + * @param withSkill The Skill value. + */ + public void setSkill(BotFrameworkSkill withSkill) { + this.skill = withSkill; + } + /** + * Gets an instance of a {@link SkillConversationIdFactoryBase} + * used to generate conversation IDs for interacting with the skill. + * @return the ConversationIdFactory value as a SkillConversationIdFactoryBase. + */ + public SkillConversationIdFactoryBase getConversationIdFactory() { + return this.conversationIdFactory; + } + + /** + * Sets an instance of a {@link SkillConversationIdFactoryBase} + * used to generate conversation IDs for interacting with the skill. + * @param withConversationIdFactory The ConversationIdFactory value. + */ + public void setConversationIdFactory(SkillConversationIdFactoryBase withConversationIdFactory) { + this.conversationIdFactory = withConversationIdFactory; + } + /** + * Gets the {@link ConversationState} to be used by the dialog. + * @return the ConversationState value as a getConversationState(). + */ + public ConversationState getConversationState() { + return this.conversationState; + } + + /** + * Sets the {@link ConversationState} to be used by the dialog. + * @param withConversationState The ConversationState value. + */ + public void setConversationState(ConversationState withConversationState) { + this.conversationState = withConversationState; + } + /** + * Gets the OAuth Connection Name, that would be used to perform + * Single SignOn with a skill. + * @return the ConnectionName value as a String. + */ + public String getConnectionName() { + return this.connectionName; + } + + /** + * Sets the OAuth Connection Name, that would be used to perform + * Single SignOn with a skill. + * @param withConnectionName The ConnectionName value. + */ + public void setConnectionName(String withConnectionName) { + this.connectionName = withConnectionName; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillInvokeException.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillInvokeException.java new file mode 100644 index 000000000..bb04873ff --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillInvokeException.java @@ -0,0 +1,41 @@ +package com.microsoft.bot.dialogs; + +/** + * Exception used to report issues during the invoke method of the {@link SkillDialog} class. + */ +public class SkillInvokeException extends RuntimeException { + + /** + * Serial Version for class. + */ + private static final long serialVersionUID = 1L; + + /** + * Construct with exception. + * + * @param t The cause. + */ + public SkillInvokeException(Throwable t) { + super(t); + } + + /** + * Construct with message. + * + * @param message The exception message. + */ + public SkillInvokeException(String message) { + super(message); + } + + /** + * Construct with caught exception and message. + * + * @param message The message. + * @param t The caught exception. + */ + public SkillInvokeException(String message, Throwable t) { + super(message, t); + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ThisPath.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ThisPath.java new file mode 100644 index 000000000..b4b69f417 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ThisPath.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Defines path passed to the active dialog. + */ +public final class ThisPath { + + private ThisPath() { + } + + /** + * The options that were passed to the active dialog via options argument of + * BeginDialog. + */ + public static final String OPTIONS = "this.options"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/TurnPath.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/TurnPath.java new file mode 100644 index 000000000..17d053f1c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/TurnPath.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +/** + * Defines path for avaiable turns. + */ +public final class TurnPath { + private TurnPath() { } + + /// The result from the last dialog that was called. + public static final String LAST_RESULT = "turn.lastresult"; + + /// The current activity for the turn. + public static final String ACTIVITY = "turn.activity"; + + /// The recognized result for the current turn. + public static final String RECOGNIZED = "turn.recognized"; + + /// Path to the top intent. + public static final String TOP_INTENT = "turn.recognized.intent"; + + /// Path to the top score. + public static final String TOP_SCORE = "turn.recognized.score"; + + /// Original text. + public static final String TEXT = "turn.recognized.text"; + + /// Original utterance split into unrecognized strings. + public static final String UNRECOGNIZED_TEXT = "turn.unrecognizedText"; + + /// Entities that were recognized from text. + public static final String RECOGNIZED_ENTITIES = "turn.recognizedEntities"; + + /// If true an interruption has occured. + public static final String INTERRUPTED = "turn.interrupted"; + + /// The current dialog event (set during event processing). + public static final String DIALOG_EVENT = "turn.dialogEvent"; + + /// Used to track that we don't end up in infinite loop of RepeatDialogs(). + public static final String REPEATED_IDS = "turn.repeatedIds"; + + /// This is a bool which if set means that the turncontext.activity has been consumed by + // some component in the system. + public static final String ACTIVITY_PROCESSED = "turn.activityProcessed"; + + /** + * Utility function to get just the property name without the memory scope prefix. + * @param property memory scope property path. + * @return name of the property without the prefix. + */ + public static String getPropertyName(String property) { + return property.replace("turn.", ""); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java new file mode 100644 index 000000000..679a8fc19 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.ActivityTypes; + +import org.apache.commons.lang3.StringUtils; + +/** + * Dialog optimized for prompting a user with a series of questions. Waterfalls + * accept a stack of functions which will be executed in sequence. Each + * waterfall step can ask a question of the user and the user's response will be + * passed as an argument to the next waterfall step. + */ +public class WaterfallDialog extends Dialog { + + private static final String PERSISTED_OPTIONS = "options"; + private static final String PERSISTED_VALUES = "values"; + private static final String PERSISTED_INSTANCEID = "instanceId"; + private static final String STEP_INDEX = "stepIndex"; + + private final List steps; + + /** + * Initializes a new instance of the {@link WaterfallDialog} class. + * + * @param dialogId The dialog ID. + * @param actions Optional actions to be defined by the caller. + */ + public WaterfallDialog(String dialogId, List actions) { + super(dialogId); + steps = actions != null ? actions : new ArrayList(); + } + + /** + * Gets a unique String which represents the version of this dialog. If the + * version changes between turns the dialog system will emit a DialogChanged + * event. + * + * @return Version will change when steps count changes (because dialog has no + * way of evaluating the content of the steps. + */ + public String getVersion() { + return String.format("%s:%d", getId(), steps.size()); + } + + /** + * Adds a new step to the waterfall. + * + * @param step Step to add. + * @return Waterfall dialog for fluent calls to `AddStep()`. + */ + public WaterfallDialog addStep(WaterfallStep step) { + if (step == null) { + throw new IllegalArgumentException("step cannot be null"); + } + steps.add(step); + return this; + } + + /** + * Called when the waterfall dialog is started and pushed onto the dialog stack. + * + * @param dc The + * @param options Optional, initial information to pass to the dialog. + * @return A CompletableFuture representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + @Override + public CompletableFuture beginDialog(DialogContext dc, Object options) { + + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "dc cannot be null." + )); + } + + // Initialize waterfall state + Map state = dc.getActiveDialog().getState(); + String instanceId = UUID.randomUUID().toString(); + state.put(PERSISTED_OPTIONS, options); + state.put(PERSISTED_VALUES, new HashMap()); + state.put(PERSISTED_INSTANCEID, instanceId); + + Map properties = new HashMap(); + properties.put("DialogId", getId()); + properties.put("InstanceId", instanceId); + + getTelemetryClient().trackEvent("WaterfallStart", properties); + getTelemetryClient().trackDialogView(getId(), null, null); + + // Run first step + return runStep(dc, 0, DialogReason.BEGIN_CALLED, null); + } + + /** + * Called when the waterfall dialog is _continued_, where it is the active + * dialog and the user replies with a new activity. + * + * @param dc The + * @return A CompletableFuture representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * result may also contain a return value. + */ + @Override + public CompletableFuture continueDialog(DialogContext dc) { + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "dc cannot be null." + )); + } + + // Don't do anything for non-message activities. + if (!dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + return CompletableFuture.completedFuture(END_OF_TURN); + } + + // Run next step with the message text as the result. + return resumeDialog(dc, DialogReason.CONTINUE_CALLED); + } + + /** + * Called when a child waterfall dialog completed its turn, returning control to + * this dialog. + * + * @param dc The dialog context for the current turn of the conversation. + * @param reason Reason why the dialog resumed. + * @param result Optional, value returned from the dialog that was called. The + * type of the value returned is dependent on the child dialog. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "dc cannot be null." + )); + } + + // Increment step index and run step + Map state = dc.getActiveDialog().getState(); + int index = 0; + if (state.containsKey(STEP_INDEX)) { + index = (int) state.get(STEP_INDEX); + } + + return runStep(dc, index + 1, reason, result); + } + + /** + * Called when the dialog is ending. + * + * @param turnContext Context for the current turn of the conversation. + * @param instance The instance of the current dialog. + * @param reason The reason the dialog is ending. + * + * @return A task that represents the work queued to execute. + */ + @Override + public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) { + if (reason == DialogReason.CANCEL_CALLED) { + HashMap state = new HashMap((Map) instance.getState()); + + // Create step context + int index = (int) state.get(STEP_INDEX); + String stepName = waterfallStepName(index); + String instanceId = (String) state.get(PERSISTED_INSTANCEID); + + HashMap properties = new HashMap(); + properties.put("DialogId", getId()); + properties.put("StepName", stepName); + properties.put("InstanceId", instanceId); + + getTelemetryClient().trackEvent("WaterfallCancel", properties); + } else if (reason == DialogReason.END_CALLED) { + HashMap state = new HashMap((Map) instance.getState()); + String instanceId = (String) state.get(PERSISTED_INSTANCEID); + + HashMap properties = new HashMap(); + properties.put("DialogId", getId()); + properties.put("InstanceId", instanceId); + getTelemetryClient().trackEvent("WaterfallComplete", properties); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Called when an individual waterfall step is being executed. + * + * @param stepContext Context for the waterfall step to execute. + * + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture onStep(WaterfallStepContext stepContext) { + String stepName = waterfallStepName(stepContext.getIndex()); + String instanceId = (String) stepContext.getActiveDialog().getState().get(PERSISTED_INSTANCEID); + + HashMap properties = new HashMap(); + properties.put("DialogId", getId()); + properties.put("StepName", stepName); + properties.put("InstanceId", instanceId); + + getTelemetryClient().trackEvent("WaterfallStep", properties); + + return steps.get(stepContext.getIndex()).waterfallStep(stepContext); + } + + /** + * Excutes a step of the waterfall dialog. + * + * @param dc The {@link DialogContext} for the current turn of conversation. + * @param index The index of the current waterfall step to execute. + * @param reason The reason the waterfall step is being executed. + * @param result Result returned by a dialog called in the previous waterfall step. + * + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture runStep(DialogContext dc, int index, + DialogReason reason, Object result) { + + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "dc cannot be null." + )); + } + + if (index < steps.size()) { + // Update persisted step index + Map state = (Map) dc.getActiveDialog().getState(); + + state.put(STEP_INDEX, index); + + // Create step context + Object options = state.get(PERSISTED_OPTIONS); + Map values = (Map) state.get(PERSISTED_VALUES); + WaterfallStepContext stepContext = + new WaterfallStepContext(this, dc, options, values, index, reason, result); + + // Execute step + return onStep(stepContext); + } + + // End of waterfall so just return any result to parent + return dc.endDialog(result); + } + + private String waterfallStepName(int index) { + // Log Waterfall Step event. Each event has a distinct name to hook up + // to the Application Insights funnel. + String stepName = steps.get(index).getClass().getSimpleName(); + + // Default stepname for lambdas + if (StringUtils.isAllBlank(stepName) || stepName.contains("$Lambda$")) { + stepName = String.format("Step%dof%d", index + 1, steps.size()); + } + + return stepName; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallStep.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallStep.java new file mode 100644 index 000000000..37266c05f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallStep.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.concurrent.CompletableFuture; + +/** + * A interface definition of a Waterfall step. This is implemented by + * application code. + */ +public interface WaterfallStep { + /** + * A interface definition of a Waterfall step. This is implemented by + * application code. + * + * @param stepContext The WaterfallStepContext for this waterfall dialog. + * + * @return A {@link CompletableFuture} of {@link DialogTurnResult} representing + * the asynchronous operation. + */ + CompletableFuture waterfallStep(WaterfallStepContext stepContext); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallStepContext.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallStepContext.java new file mode 100644 index 000000000..9a3a325ee --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallStepContext.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.Async; + +/** + * Provides context for a step in a {@link WaterfallDialog} . + * + * The {@link DialogContext} property contains the {@link TurnContext} + * for the current turn. + */ +public class WaterfallStepContext extends DialogContext { + + private final WaterfallDialog parentWaterfall; + private Boolean nextCalled; + + /** + * Initializes a new instance of the {@link WaterfallStepContext} class. + * + * @param parentWaterfall The parent of the waterfall dialog. + * @param dc The dialog's context. + * @param options Any options to call the waterfall dialog with. + * @param values A dictionary of values which will be persisted across all + * waterfall steps. + * @param index The index of the current waterfall to execute. + * @param reason The reason the waterfall step is being executed. + * @param result Results returned by a dialog called in the previous waterfall + * step. + */ + public WaterfallStepContext( + WaterfallDialog parentWaterfall, + DialogContext dc, + Object options, + Map values, + int index, + DialogReason reason, + Object result) { + super(dc.getDialogs(), dc, new DialogState(dc.getStack())); + this.parentWaterfall = parentWaterfall; + this.nextCalled = false; + this.setParent(dc.getParent()); + this.index = index; + this.options = options; + this.reason = reason; + this.result = result; + this.values = values; + } + + private int index; + + private Object options; + + private DialogReason reason; + + private Object result; + + private Map values; + + /** + * Gets the index of the current waterfall step being executed. + * @return returns the index value; + */ + public int getIndex() { + return this.index; + } + + /** + * Gets any options the waterfall dialog was called with. + * @return The options. + */ + public Object getOptions() { + return this.options; + } + + /** + * Gets the reason the waterfall step is being executed. + * @return The DialogReason + */ + public DialogReason getReason() { + return this.reason; + } + + /** + * Gets the result from the previous waterfall step. + * + * The result is often the return value of a child dialog that was started in the previous step + * of the waterfall. + * @return the Result value. + */ + public Object getResult() { + return this.result; + } + + /** + * Gets a dictionary of values which will be persisted across all waterfall actions. + * @return The Dictionary of values. + */ + public Map getValues() { + return this.values; + } + + /** + * Skips to the next step of the waterfall. + * + * @param result Optional, result to pass to the next step of the current waterfall + * dialog. + * @return A CompletableFuture that represents the work queued to execute. + * + * In the next step of the waterfall, the {@link result} property of the waterfall step context + * will contain the value of the . + */ + + public CompletableFuture next(Object result) { + // Ensure next hasn't been called + if (nextCalled) { + return Async.completeExceptionally(new IllegalStateException( + String.format("WaterfallStepContext.next(): method already called for dialog and step '%0 %1", + parentWaterfall.getId(), index) + )); + } + + // Trigger next step + nextCalled = true; + return parentWaterfall.resumeDialog(this, DialogReason.NEXT_CALLED, result); + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Channel.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Channel.java new file mode 100644 index 000000000..647bfd4bf --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Channel.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Channels; +import org.apache.commons.lang3.StringUtils; + +/** + * Methods for determining channel specific functionality. + */ +public final class Channel { + private Channel() { } + + /** + * Determine if a number of Suggested Actions are supported by a Channel. + * + * @param channelId The Channel to check the if Suggested Actions are supported in. + * @return True if the Channel supports the buttonCnt total Suggested Actions, False if + * the Channel does not support that number of Suggested Actions. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static boolean supportsSuggestedActions(String channelId) { + return supportsSuggestedActions(channelId, 100); + } + + /** + * Determine if a number of Suggested Actions are supported by a Channel. + * + * @param channelId The Channel to check the if Suggested Actions are supported in. + * @param buttonCnt The number of Suggested Actions to check for the Channel. + * @return True if the Channel supports the buttonCnt total Suggested Actions, False if + * the Channel does not support that number of Suggested Actions. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static boolean supportsSuggestedActions(String channelId, int buttonCnt) { + switch (channelId) { + // https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies + case Channels.FACEBOOK: + case Channels.SKYPE: + return buttonCnt <= 10; + + // https://developers.line.biz/en/reference/messaging-api/#items-object + case Channels.LINE: + return buttonCnt <= 13; + + // https://dev.kik.com/#/docs/messaging#text-response-object + case Channels.KIK: + return buttonCnt <= 20; + + case Channels.TELEGRAM: + case Channels.EMULATOR: + case Channels.DIRECTLINE: + case Channels.DIRECTLINESPEECH: + case Channels.WEBCHAT: + return buttonCnt <= 100; + + default: + return false; + } + } + + /** + * Determine if a number of Card Actions are supported by a Channel. + * + * @param channelId The Channel to check if the Card Actions are supported in. + * @return True if the Channel supports the buttonCnt total Card Actions, False if the + * Channel does not support that number of Card Actions. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static boolean supportsCardActions(String channelId) { + return supportsCardActions(channelId, 100); + } + + /** + * Determine if a number of Card Actions are supported by a Channel. + * + * @param channelId The Channel to check if the Card Actions are supported in. + * @param buttonCnt The number of Card Actions to check for the Channel. + * @return True if the Channel supports the buttonCnt total Card Actions, False if the + * Channel does not support that number of Card Actions. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static boolean supportsCardActions(String channelId, int buttonCnt) { + switch (channelId) { + case Channels.FACEBOOK: + case Channels.SKYPE: + case Channels.MSTEAMS: + return buttonCnt <= 3; + + case Channels.LINE: + return buttonCnt <= 99; + + case Channels.SLACK: + case Channels.EMULATOR: + case Channels.DIRECTLINE: + case Channels.DIRECTLINESPEECH: + case Channels.WEBCHAT: + case Channels.CORTANA: + return buttonCnt <= 100; + + default: + return false; + } + } + + /** + * Determine if a Channel has a Message Feed. + * @param channelId The Channel to check for Message Feed. + * @return True if the Channel has a Message Feed, False if it does not. + */ + public static boolean hasMessageFeed(String channelId) { + switch (channelId) { + case Channels.CORTANA: + return false; + + default: + return true; + } + } + + /** + * Maximum length allowed for Action Titles. + * + * @param channelId The Channel to determine Maximum Action Title Length. + * @return The total number of characters allowed for an Action Title on a specific Channel. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static int maxActionTitleLength(String channelId) { + return 20; + } + + /** + * Get the Channel Id from the current Activity on the Turn Context. + * + * @param turnContext The Turn Context to retrieve the Activity's Channel Id from. + * @return The Channel Id from the Turn Context's Activity. + */ + public static String getChannelId(TurnContext turnContext) { + return StringUtils.isEmpty(turnContext.getActivity().getChannelId()) + ? null + : turnContext.getActivity().getChannelId(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Choice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Choice.java new file mode 100644 index 000000000..153cebb20 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Choice.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.bot.schema.CardAction; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a choice for a choice prompt. + */ +public class Choice { + @JsonProperty(value = "value") + private String value; + + @JsonProperty(value = "action") + private CardAction action; + + @JsonProperty(value = "synonyms") + private List synonyms; + + /** + * Creates a Choice. + */ + public Choice() { + this(null); + } + + /** + * Creates a Choice. + * @param withValue The value. + */ + public Choice(String withValue) { + value = withValue; + } + + /** + * Creates a Choice. + * @param withValue The value. + * @param withSynonyms The list of synonyms to recognize in addition to the value. + */ + public Choice(String withValue, List withSynonyms) { + value = withValue; + synonyms = withSynonyms; + } + + /** + * Creates a Choice. + * @param withValue The value. + * @param withSynonyms The list of synonyms to recognize in addition to the value. + */ + public Choice(String withValue, String... withSynonyms) { + value = withValue; + synonyms = Arrays.asList(withSynonyms); + } + + /** + * Gets the value to return when selected. + * @return The value. + */ + public String getValue() { + return value; + } + + /** + * Sets the value to return when selected. + * @param withValue The value to return. + */ + public void setValue(String withValue) { + value = withValue; + } + + /** + * Gets the action to use when rendering the choice as a suggested action or hero card. + * @return The action to use. + */ + public CardAction getAction() { + return action; + } + + /** + * Sets the action to use when rendering the choice as a suggested action or hero card. + * @param withAction The action to use. + */ + public void setAction(CardAction withAction) { + action = withAction; + } + + /** + * Gets the list of synonyms to recognize in addition to the value. This is optional. + * @return The list of synonyms. + */ + public List getSynonyms() { + return synonyms; + } + + /** + * Sets the list of synonyms to recognize in addition to the value. This is optional. + * @param withSynonyms The list of synonyms. + */ + public void setSynonyms(List withSynonyms) { + synonyms = withSynonyms; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java new file mode 100644 index 000000000..c59d452b5 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactory.java @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.CardAction; +import com.microsoft.bot.schema.HeroCard; +import com.microsoft.bot.schema.InputHints; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + +/** + * Assists with formatting a message activity that contains a list of choices. + */ +public final class ChoiceFactory { + private ChoiceFactory() { + } + + /** + * Creates an Activity that includes a list of choices formatted based on the + * capabilities of a given channel. + * + * @param channelId A channel ID. The Connector.Channels class contains known + * channel IDs. + * @param list The list of choices to include. + * @param text The text of the message to send. Can be null. + * @return The created Activity + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static Activity forChannel(String channelId, List list, String text) { + return forChannel(channelId, list, text, null, null); + } + + /** + * Creates an Activity that includes a list of choices formatted based on the + * capabilities of a given channel. + * + * @param channelId A channel ID. The Connector.Channels class contains known + * channel IDs. + * @param list The list of choices to include. + * @param text The text of the message to send. Can be null. + * @param speak The text to be spoken by your bot on a speech-enabled + * channel. Can be null. + * @param options The formatting options to use when rendering as a list. If + * null, the default options are used. + * @return The created Activity + */ + @SuppressWarnings("checkstyle:MagicNumber") + public static Activity forChannel(String channelId, List list, String text, String speak, + ChoiceFactoryOptions options) { + if (list == null) { + list = new ArrayList<>(); + } + + // Find maximum title length + int maxTitleLength = 0; + for (Choice choice : list) { + int l = choice.getAction() != null && !StringUtils.isEmpty(choice.getAction().getTitle()) + ? choice.getAction().getTitle().length() + : choice.getValue().length(); + + if (l > maxTitleLength) { + maxTitleLength = l; + } + } + + // Determine list style + boolean supportsSuggestedActions = Channel.supportsSuggestedActions(channelId, list.size()); + boolean supportsCardActions = Channel.supportsCardActions(channelId, list.size()); + int maxActionTitleLength = Channel.maxActionTitleLength(channelId); + boolean longTitles = maxTitleLength > maxActionTitleLength; + + if (!longTitles && !supportsSuggestedActions && supportsCardActions) { + // SuggestedActions is the preferred approach, but for channels that don't + // support them (e.g. Teams, Cortana) we should use a HeroCard with CardActions + return heroCard(list, text, speak); + } + + if (!longTitles && supportsSuggestedActions) { + // We always prefer showing choices using suggested actions. If the titles are + // too long, however, + // we'll have to show them as a text list. + return suggestedAction(list, text, speak); + } + + if (!longTitles && list.size() <= 3) { + // If the titles are short and there are 3 or less choices we'll use an inline + // list. + return inline(list, text, speak, options); + } + + // Show a numbered list. + return list(list, text, speak, options); + } + + /** + * Creates an Activity that includes a list of choices formatted as an inline + * list. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. Can be null. + * @return The created Activity. + */ + public static Activity inline(List choices, String text) { + return inline(choices, text, null, null); + } + + /** + * Creates an Activity that includes a list of choices formatted as an inline + * list. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. Can be null. + * @param speak The text to be spoken by your bot on a speech-enabled channel. + * Cab be null. + * @param options The formatting options to use when rendering as a list. Can be + * null. + * @return The created Activity. + */ + public static Activity inline(List choices, String text, String speak, ChoiceFactoryOptions options) { + if (choices == null) { + choices = new ArrayList<>(); + } + + // clone options with defaults applied if needed. + ChoiceFactoryOptions opt = new ChoiceFactoryOptions(options); + + // Format list of choices + String connector = ""; + StringBuilder txtBuilder; + if (StringUtils.isNotBlank(text)) { + txtBuilder = new StringBuilder(text).append(' '); + } else { + txtBuilder = new StringBuilder().append(' '); + } + + for (int index = 0; index < choices.size(); index++) { + Choice choice = choices.get(index); + String title = choice.getAction() != null && choice.getAction().getTitle() != null + ? choice.getAction().getTitle() + : choice.getValue(); + + txtBuilder.append(connector); + if (opt.getIncludeNumbers()) { + txtBuilder.append('(').append(index + 1).append(") "); + } + + txtBuilder.append(title); + if (index == choices.size() - 2) { + connector = index == 0 ? opt.getInlineOr() : opt.getInlineOrMore(); + } else { + connector = opt.getInlineSeparator(); + } + } + + // Return activity with choices as an inline list. + return MessageFactory.text(txtBuilder.toString(), speak, InputHints.EXPECTING_INPUT); + } + + /** + * Creates a message activity that includes a list of choices formatted as a + * numbered or bulleted list. + * + * @param choices The list of strings to include as Choices. + * @return The created Activity. + */ + public static Activity listFromStrings(List choices) { + return listFromStrings(choices, null, null, null); + } + + /** + * Creates a message activity that includes a list of choices formatted as a + * numbered or bulleted list. + * + * @param choices The list of strings to include as Choices. + * @param text The text of the message to send. + * @param speak The text to be spoken by your bot on a speech-enabled channel. + * @param options The formatting options to use when rendering as a list. + * @return The created Activity. + */ + public static Activity listFromStrings(List choices, String text, String speak, + ChoiceFactoryOptions options) { + return list(toChoices(choices), text, speak, options); + } + + /** + * Creates a message activity that includes a list of choices formatted as a + * numbered or bulleted list. + * + * @param choices The list of choices to include. + * @return The created Activity. + */ + public static Activity list(List choices) { + return list(choices, null, null, null); + } + + /** + * Creates a message activity that includes a list of choices formatted as a + * numbered or bulleted list. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. + * @return The created Activity. + */ + public static Activity list(List choices, String text) { + return list(choices, text, null, null); + } + + /** + * Creates a message activity that includes a list of choices formatted as a + * numbered or bulleted list. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. + * @param speak The text to be spoken by your bot on a speech-enabled channel. + * @param options The formatting options to use when rendering as a list. + * @return The created Activity. + */ + public static Activity list(List choices, String text, String speak, ChoiceFactoryOptions options) { + if (choices == null) { + choices = new ArrayList<>(); + } + + // clone options with defaults applied if needed. + ChoiceFactoryOptions opt = new ChoiceFactoryOptions(options); + + boolean includeNumbers = opt.getIncludeNumbers(); + + // Format list of choices + String connector = ""; + StringBuilder txtBuilder = text == null ? new StringBuilder() : new StringBuilder(text).append("\n\n "); + + for (int index = 0; index < choices.size(); index++) { + Choice choice = choices.get(index); + + String title = choice.getAction() != null && !StringUtils.isEmpty(choice.getAction().getTitle()) + ? choice.getAction().getTitle() + : choice.getValue(); + + txtBuilder.append(connector); + if (includeNumbers) { + txtBuilder.append(index + 1).append(". "); + } else { + txtBuilder.append("- "); + } + + txtBuilder.append(title); + connector = "\n "; + } + + // Return activity with choices as a numbered list. + return MessageFactory.text(txtBuilder.toString(), speak, InputHints.EXPECTING_INPUT); + } + + /** + * Creates an Activity that includes a list of card actions. + * + * @param choices The list of strings to include as actions. + * @return The created Activity. + */ + public static Activity suggestedActionFromStrings(List choices) { + return suggestedActionFromStrings(choices, null, null); + } + + /** + * Creates an Activity that includes a list of card actions. + * + * @param choices The list of strings to include as actions. + * @param text The text of the message to send. + * @param speak The text to be spoken by your bot on a speech-enabled channel. + * @return The created Activity. + */ + public static Activity suggestedActionFromStrings(List choices, String text, String speak) { + return suggestedAction(toChoices(choices), text, speak); + } + + /** + * Creates an Activity that includes a list of card actions. + * + * @param choices The list of choices to include. + * @return The created Activity. + */ + public static Activity suggestedAction(List choices) { + return suggestedAction(choices, null, null); + } + + /** + * Creates an Activity that includes a list of card actions. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. + * @return The created Activity. + */ + public static Activity suggestedAction(List choices, String text) { + return suggestedAction(choices, text, null); + } + + /** + * Creates an Activity that includes a list of card actions. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. + * @param speak The text to be spoken by your bot on a speech-enabled channel. + * @return The created Activity. + */ + public static Activity suggestedAction(List choices, String text, String speak) { + // Return activity with choices as suggested actions + return MessageFactory.suggestedCardActions(extractActions(choices), text, speak, InputHints.EXPECTING_INPUT); + } + + /** + * Creates an Activity with a HeroCard based on a list of Choices. + * + * @param choices The list of choices to include. + * @return The created Activity. + */ + public static Activity heroCard(List choices) { + return heroCard(choices, null, null); + } + + /** + * Creates an Activity with a HeroCard based on a list of Choices. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. + * @return The created Activity. + */ + public static Activity heroCard(List choices, String text) { + return heroCard(choices, text, null); + } + + /** + * Creates an Activity with a HeroCard based on a list of Choices. + * + * @param choices The list of choices to include. + * @param text The text of the message to send. + * @param speak The text to be spoken by your bot on a speech-enabled channel. + * @return The created Activity. + */ + public static Activity heroCard(List choices, String text, String speak) { + HeroCard card = new HeroCard(); + card.setText(text); + card.setButtons(extractActions(choices)); + + List attachments = new ArrayList() { + /** + * + */ + private static final long serialVersionUID = 1L; + + { + add(card.toAttachment()); + } + }; + + // Return activity with choices as HeroCard with buttons + return MessageFactory.attachment(attachments, null, speak, InputHints.EXPECTING_INPUT); + } + + /** + * Returns a list of strings as a list of Choices. + * + * @param choices The list of strings to convert. + * @return A List of Choices. + */ + public static List toChoices(List choices) { + return choices == null ? new ArrayList<>() : choices.stream().map(Choice::new).collect(Collectors.toList()); + } + + /** + * Returns a list of strings as a list of Choices. + * + * @param choices The strings to convert. + * @return A List of Choices. + */ + public static List toChoices(String... choices) { + return toChoices(Arrays.asList(choices)); + } + + private static List extractActions(List choices) { + if (choices == null) { + choices = new ArrayList<>(); + } + + // Map choices to actions + return choices.stream().map(choice -> { + if (choice.getAction() != null) { + return choice.getAction(); + } + + CardAction card = new CardAction(); + card.setType(ActionTypes.IM_BACK); + card.setValue(choice.getValue()); + card.setTitle(choice.getValue()); + return card; + }).collect(Collectors.toList()); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactoryOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactoryOptions.java new file mode 100644 index 000000000..62a20dbda --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceFactoryOptions.java @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; + +/** + * Contains formatting options for presenting a list of choices. + */ +public class ChoiceFactoryOptions { + public static final String DEFAULT_INLINE_SEPERATOR = ", "; + public static final String DEFAULT_INLINE_OR = " or "; + public static final String DEFAULT_INLINE_OR_MORE = ", or "; + public static final boolean DEFAULT_INCLUDE_NUMBERS = true; + + /** + * Creates default options. + */ + public ChoiceFactoryOptions() { + this(DEFAULT_INLINE_SEPERATOR, DEFAULT_INLINE_OR, DEFAULT_INLINE_OR_MORE); + } + + /** + * Clones another options object, and applies defaults if needed. + * + * @param options The options object to clone. + */ + public ChoiceFactoryOptions(ChoiceFactoryOptions options) { + this(); + + if (options != null) { + if (!StringUtils.isEmpty(options.getInlineSeparator())) { + setInlineSeparator(options.getInlineSeparator()); + } + + if (!StringUtils.isEmpty(options.getInlineOr())) { + setInlineOr(options.getInlineOr()); + } + + if (!StringUtils.isEmpty(options.getInlineOrMore())) { + setInlineOrMore(options.getInlineOrMore()); + } + + setIncludeNumbers(options.getIncludeNumbers()); + } + } + + /** + * Creates options with the specified formatting values. + * @param withInlineSeparator The inline seperator value. + * @param withInlineOr The inline or value. + * @param withInlineOrMore The inline or more value. + */ + public ChoiceFactoryOptions( + String withInlineSeparator, + String withInlineOr, + String withInlineOrMore + ) { + this(withInlineSeparator, withInlineOr, withInlineOrMore, DEFAULT_INCLUDE_NUMBERS); + } + + /** + * Initializes a new instance of the class. + * Refer to the code in teh ConfirmPrompt for an example of usage. + * @param withInlineSeparator The inline seperator value. + * @param withInlineOr The inline or value. + * @param withInlineOrMore The inline or more value. + * @param withIncludeNumbers Flag indicating whether to include numbers as a choice. + */ + public ChoiceFactoryOptions( + String withInlineSeparator, + String withInlineOr, + String withInlineOrMore, + boolean withIncludeNumbers + ) { + inlineSeparator = withInlineSeparator; + inlineOr = withInlineOr; + inlineOrMore = withInlineOrMore; + includeNumbers = withIncludeNumbers; + } + + @JsonProperty(value = "inlineSeparator") + private String inlineSeparator; + + @JsonProperty(value = "inlineOr") + private String inlineOr; + + @JsonProperty(value = "inlineOrMore") + private String inlineOrMore; + + @JsonProperty(value = "includeNumbers") + private Boolean includeNumbers; + + /** + * Gets the character used to separate individual choices when there are more than 2 choices. + * The default value is `", "`. This is optional. + * + * @return The seperator. + */ + public String getInlineSeparator() { + return inlineSeparator; + } + + /** + * Sets the character used to separate individual choices when there are more than 2 choices. + * @param withSeperator The seperator. + */ + public void setInlineSeparator(String withSeperator) { + inlineSeparator = withSeperator; + } + + /** + * Gets the separator inserted between the choices when their are only 2 choices. The default + * value is `" or "`. This is optional. + * + * @return The separator inserted between the choices when their are only 2 choices. + */ + public String getInlineOr() { + return inlineOr; + } + + /** + * Sets the separator inserted between the choices when their are only 2 choices. + * + * @param withInlineOr The separator inserted between the choices when their are only 2 choices. + */ + public void setInlineOr(String withInlineOr) { + this.inlineOr = withInlineOr; + } + + /** + * Gets the separator inserted between the last 2 choices when their are more than 2 choices. + * The default value is `", or "`. This is optional. + * + * @return The separator inserted between the last 2 choices when their are more than 2 choices. + */ + public String getInlineOrMore() { + return inlineOrMore; + } + + /** + * Sets the separator inserted between the last 2 choices when their are more than 2 choices. + * + * @param withInlineOrMore The separator inserted between the last 2 choices when their + * are more than 2 choices. + */ + public void setInlineOrMore(String withInlineOrMore) { + this.inlineOrMore = withInlineOrMore; + } + + /** + * Gets a value indicating whether an inline and list style choices will be prefixed + * with the index of the choice; as in "1. choice". If false, the list style will use a + * bulleted list instead. The default value is true. + * + * @return If false, the list style will use a bulleted list. + */ + public Boolean getIncludeNumbers() { + return includeNumbers; + } + + /** + * Sets the value indicating whether an inline and list style choices will be prefixed + * with the index of the choice. + * + * @param withIncludeNumbers If false, the list style will use a bulleted list instead. + */ + public void setIncludeNumbers(Boolean withIncludeNumbers) { + this.includeNumbers = withIncludeNumbers; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceRecognizers.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceRecognizers.java new file mode 100644 index 000000000..f0e950a50 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ChoiceRecognizers.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.microsoft.recognizers.text.IModel; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import com.microsoft.recognizers.text.number.NumberRecognizer; + +/** + * Contains methods for matching user input against a list of choices. + */ +public final class ChoiceRecognizers { + private ChoiceRecognizers() { } + + /** + * Matches user input against a list of choices. + * @param utterance The input. + * @param choices The list of string choices. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> recognizeChoicesFromStrings(String utterance, List choices) { + return recognizeChoicesFromStrings(utterance, choices, null); + } + + /** + * Matches user input against a list of choices. + * @param utterance The input. + * @param choices The list of string choices. + * @param options Optional, options to control the recognition strategy. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> recognizeChoicesFromStrings( + String utterance, + List choices, + FindChoicesOptions options + ) { + return recognizeChoices(utterance, choices.stream().map(Choice::new) + .collect(Collectors.toList()), options); + } + + /** + * Matches user input against a list of choices. + * @param utterance The input. + * @param choices The list of string choices. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> recognizeChoices(String utterance, List choices) { + return recognizeChoices(utterance, choices, null); + } + + /** + * Matches user input against a list of choices. + * @param utterance The input. + * @param choices The list of string choices. + * @param options Optional, options to control the recognition strategy. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> recognizeChoices( + String utterance, + List choices, + FindChoicesOptions options + ) { + // Try finding choices by text search first + // - We only want to use a single strategy for returning results to avoid issues where utterances + // like the "the third one" or "the red one" or "the first division book" would miss-recognize as + // a numerical index or ordinal as well. + String locale = options != null ? options.getLocale() : Locale.ENGLISH.getDisplayName(); + List> matched = Find.findChoices(utterance, choices, options); + if (matched.size() == 0) { + List> matches = new ArrayList<>(); + if (options == null || options.isRecognizeOrdinals()) { + // Next try finding by ordinal + matches = recognizeNumbers(utterance, new NumberRecognizer(locale).getOrdinalModel(locale, true)); + for (ModelResult match : matches) { + matchChoiceByIndex(choices, matched, match); + } + } + + if (matches.size() == 0 && (options == null || options.isRecognizeNumbers())) { + // Then try by numerical index + matches = recognizeNumbers(utterance, new NumberRecognizer(locale).getNumberModel(locale, true)); + for (ModelResult match : matches) { + matchChoiceByIndex(choices, matched, match); + } + } + + // Sort any found matches by their position within the utterance. + // - The results from findChoices() are already properly sorted so we just need this + // for ordinal & numerical lookups. + matched.sort(Comparator.comparingInt(ModelResult::getStart)); + } + + return matched; + } + + private static void matchChoiceByIndex( + List list, + List> matched, + ModelResult match + ) { + try { + // converts Resolution Values containing "end" (e.g. utterance "last") in numeric values. + String value = match.getResolution().getValue().replace("end", Integer.toString(list.size())); + int index = Integer.parseInt(value) - 1; + if (index >= 0 && index < list.size()) { + Choice choice = list.get(index); + FoundChoice resolution = new FoundChoice(); + resolution.setValue(choice.getValue()); + resolution.setIndex(index); + resolution.setScore(1.0f); + ModelResult modelResult = new ModelResult(); + modelResult.setStart(match.getStart()); + modelResult.setEnd(match.getEnd()); + modelResult.setTypeName("choice"); + modelResult.setText(match.getText()); + modelResult.setResolution(resolution); + matched.add(modelResult); + } + } catch (Throwable ignored) { + // noop here, as in dotnet/node repos + } + } + + private static List> recognizeNumbers(String utterance, IModel model) { + List result = model.parse(utterance == null ? "" : utterance); + return result.stream().map(r -> { + FoundChoice resolution = new FoundChoice(); + resolution.setValue(r.resolution.get("value").toString()); + ModelResult modelResult = new ModelResult(); + modelResult.setStart(r.start); + modelResult.setEnd(r.end); + modelResult.setText(r.text); + modelResult.setResolution(resolution); + return modelResult; + }).collect(Collectors.toList()); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Find.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Find.java new file mode 100644 index 000000000..e081c8d71 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Find.java @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + +/** + * Contains methods for matching user input against a list of choices. + */ +public final class Find { + private Find() { } + + /** + * Matches user input against a list of strings. + * @param utterance The input. + * @param choices The list of choices. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> findChoicesFromStrings( + String utterance, + List choices + ) { + return findChoicesFromStrings(utterance, choices, null); + } + + /** + * Matches user input against a list of strings. + * @param utterance The input. + * @param choices The list of choices. + * @param options Optional, options to control the recognition strategy. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> findChoicesFromStrings( + String utterance, + List choices, + FindChoicesOptions options + ) { + if (choices == null) { + throw new IllegalArgumentException("choices argument is missing"); + } + + return findChoices(utterance, choices.stream().map(Choice::new) + .collect(Collectors.toList()), options); + } + + /** + * Matches user input against a list of Choices. + * @param utterance The input. + * @param choices The list of choices. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> findChoices(String utterance, List choices) { + return findChoices(utterance, choices, null); + } + + /** + * Matches user input against a list of Choices. + * @param utterance The input. + * @param choices The list of choices. + * @param options Optional, options to control the recognition strategy. + * @return A list of found choices, sorted by most relevant first. + */ + public static List> findChoices( + String utterance, + List choices, + FindChoicesOptions options + ) { + if (choices == null) { + throw new IllegalArgumentException("choices argument is missing"); + } + + FindChoicesOptions opt = options != null ? options : new FindChoicesOptions(); + + // Build up full list of synonyms to search over. + // - Each entry in the list contains the index of the choice it belongs to which will later be + // used to map the search results back to their choice. + List synonyms = new ArrayList<>(); + + for (int index = 0; index < choices.size(); index++) { + Choice choice = choices.get(index); + + if (!opt.isNoValue()) { + synonyms.add(new SortedValue(choice.getValue(), index)); + } + + if (choice.getAction() != null && choice.getAction().getTitle() != null && !opt.isNoAction()) { + synonyms.add(new SortedValue(choice.getAction().getTitle(), index)); + } + + if (choice.getSynonyms() != null) { + for (String synonym : choice.getSynonyms()) { + synonyms.add(new SortedValue(synonym, index)); + } + } + } + + // Find synonyms in utterance and map back to their choices + return findValues(utterance, synonyms, options).stream().map(v -> { + Choice choice = choices.get(v.getResolution().getIndex()); + + FoundChoice resolution = new FoundChoice(); + resolution.setValue(choice.getValue()); + resolution.setIndex(v.getResolution().getIndex()); + resolution.setScore(v.getResolution().getScore()); + resolution.setSynonym(v.getResolution().getValue()); + ModelResult modelResult = new ModelResult(); + modelResult.setStart(v.getStart()); + modelResult.setEnd(v.getEnd()); + modelResult.setTypeName("choice"); + modelResult.setText(v.getText()); + modelResult.setResolution(resolution); + return modelResult; + }).collect(Collectors.toList()); + } + + /** + * This method is internal and should not be used. + * @param utterance The input. + * @param values The values. + * @return A list of found values. + */ + static List> findValues(String utterance, List values) { + return findValues(utterance, values, null); + } + + /** + * This method is internal and should not be used. + * @param utterance The input. + * @param values The values. + * @param options The options for the search. + * @return A list of found values. + */ + static List> findValues( + String utterance, + List values, + FindValuesOptions options + ) { + // Sort values in descending order by length so that the longest value is searched over first. + List list = new ArrayList<>(values); + list.sort((a, b) -> b.getValue().length() - a.getValue().length()); + + // Search for each value within the utterance. + List> matches = new ArrayList<>(); + FindValuesOptions opt = options != null ? options : new FindValuesOptions(); + TokenizerFunction tokenizer = opt.getTokenizer() != null ? opt.getTokenizer() : new Tokenizer(); + List tokens = tokenizer.tokenize(utterance, opt.getLocale()); + int maxDistance = opt.getMaxTokenDistance(); + + for (SortedValue entry : list) { + // Find all matches for a value + // - To match "last one" in "the last time I chose the last one" we need + // to re-search the String starting from the end of the previous match. + // - The start & end position returned for the match are token positions. + int startPos = 0; + List searchedTokens = tokenizer.tokenize(entry.getValue().trim(), opt.getLocale()); + while (startPos < tokens.size()) { + ModelResult match = matchValue( + tokens, + maxDistance, + opt, + entry.getIndex(), + entry.getValue(), + searchedTokens, + startPos + ); + if (match != null) { + startPos = match.getEnd() + 1; + matches.add(match); + } else { + break; + } + } + } + + // Sort matches by score descending + matches.sort((a, b) -> Float.compare(b.getResolution().getScore(), a.getResolution().getScore())); + + // Filter out duplicate matching indexes and overlapping characters. + // - The start & end positions are token positions and need to be translated to + // character positions before returning. We also need to populate the "text" + // field as well. + List> results = new ArrayList<>(); + Set foundIndexes = new HashSet<>(); + Set usedTokens = new HashSet<>(); + + for (ModelResult match : matches) { + // Apply filters + boolean add = !foundIndexes.contains(match.getResolution().getIndex()); + for (int i = match.getStart(); i <= match.getEnd(); i++) { + if (usedTokens.contains(i)) { + add = false; + break; + } + } + + // Add to results + if (add) { + // Update filter info + foundIndexes.add(match.getResolution().getIndex()); + + for (int i = match.getStart(); i <= match.getEnd(); i++) { + usedTokens.add(i); + } + + // Translate start & end and populate text field + match.setStart(tokens.get(match.getStart()).getStart()); + match.setEnd(tokens.get(match.getEnd()).getEnd()); + match.setText(utterance.substring(match.getStart(), match.getEnd() + 1)); + results.add(match); + } + } + + // Return the results sorted by position in the utterance + results.sort((a, b) -> a.getStart() - b.getStart()); + return results; + } + + private static int indexOfToken(List tokens, Token token, int startPos) { + for (int i = startPos; i < tokens.size(); i++) { + if (StringUtils.equalsIgnoreCase(tokens.get(i).getNormalized(), token.getNormalized())) { + return i; + } + } + + return -1; + } + + private static ModelResult matchValue( + List sourceTokens, + int maxDistance, + FindValuesOptions options, + int index, + String value, + List searchedTokens, + int startPos + ) { + // Match value to utterance and calculate total deviation. + // - The tokens are matched in order so "second last" will match in + // "the second from last one" but not in "the last from the second one". + // - The total deviation is a count of the number of tokens skipped in the + // match so for the example above the number of tokens matched would be + // 2 and the total deviation would be 1. + int matched = 0; + int totalDeviation = 0; + int start = -1; + int end = -1; + for (Token token : searchedTokens) { + // Find the position of the token in the utterance. + int pos = indexOfToken(sourceTokens, token, startPos); + if (pos >= 0) { + // Calculate the distance between the current tokens position and the + // previous tokens distance. + int distance = matched > 0 ? pos - startPos : 0; + if (distance <= maxDistance) { + // Update count of tokens matched and move start pointer to search + // for next token after the current token. + matched++; + totalDeviation += distance; + startPos = pos + 1; + + // Update start & end position that will track the span of the utterance + // that's matched. + if (start < 0) { + start = pos; + } + + end = pos; + } + } + } + + // Calculate score and format result + // - The start & end positions and the results text field will be corrected by the caller. + ModelResult result = null; + + if (matched > 0 && (matched == searchedTokens.size() || options.getAllowPartialMatches())) { + // Percentage of tokens matched. If matching "second last" in + // "the second from last one" the completeness would be 1.0 since + // all tokens were found. + int completeness = matched / searchedTokens.size(); + + // Accuracy of the match. The accuracy is reduced by additional tokens + // occurring in the value that weren't in the utterance. So an utterance + // of "second last" matched against a value of "second from last" would + // result in an accuracy of 0.5. + float accuracy = (float) matched / (matched + totalDeviation); + + // The final score is simply the completeness multiplied by the accuracy. + float score = completeness * accuracy; + + // Format result + FoundValue resolution = new FoundValue(); + resolution.setValue(value); + resolution.setIndex(index); + resolution.setScore(score); + result = new ModelResult<>(); + result.setStart(start); + result.setEnd(end); + result.setTypeName("value"); + result.setResolution(resolution); + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FindChoicesOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FindChoicesOptions.java new file mode 100644 index 000000000..ff62e7296 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FindChoicesOptions.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Contains options to control how input is matched against a list of choices. + */ +public class FindChoicesOptions extends FindValuesOptions { + @JsonProperty(value = "noValue") + private boolean noValue; + + @JsonProperty(value = "noAction") + private boolean noAction; + + @JsonProperty(value = "recognizeNumbers") + private boolean recognizeNumbers = true; + + @JsonProperty(value = "recognizeOrdinals") + private boolean recognizeOrdinals; + + /** + * Indicates whether the choices value will NOT be search over. The default is false. + * @return true if the choices value will NOT be search over. + */ + public boolean isNoValue() { + return noValue; + } + + /** + * Sets whether the choices value will NOT be search over. + * @param withNoValue true if the choices value will NOT be search over. + */ + public void setNoValue(boolean withNoValue) { + noValue = withNoValue; + } + + /** + * Indicates whether the title of the choices action will NOT be searched over. The default + * is false. + * @return true if the title of the choices action will NOT be searched over. + */ + public boolean isNoAction() { + return noAction; + } + + /** + * Sets whether the title of the choices action will NOT be searched over. + * @param withNoAction true if the title of the choices action will NOT be searched over. + */ + public void setNoAction(boolean withNoAction) { + noAction = withNoAction; + } + + /** + * Indicates whether the recognizer should check for Numbers using the NumberRecognizer's + * NumberModel. + * @return Default is true. If false, the Number Model will not be used to check the + * utterance for numbers. + */ + public boolean isRecognizeNumbers() { + return recognizeNumbers; + } + + /** + * Set whether the recognizer should check for Numbers using the NumberRecognizer's + * NumberModel. + * @param withRecognizeNumbers Default is true. If false, the Number Model will not be + * used to check the utterance for numbers. + */ + public void setRecognizeNumbers(boolean withRecognizeNumbers) { + recognizeNumbers = withRecognizeNumbers; + } + + /** + * Indicates whether the recognizer should check for Ordinal Numbers using the NumberRecognizer's + * OrdinalModel. + * @return Default is true. If false, the Ordinal Model will not be used to check the + * utterance for ordinal numbers. + */ + public boolean isRecognizeOrdinals() { + return recognizeOrdinals; + } + + /** + * Sets whether the recognizer should check for Ordinal Numbers using the NumberRecognizer's + * OrdinalModel. + * @param withRecognizeOrdinals If false, the Ordinal Model will not be used to check the + * utterance for ordinal numbers. + */ + public void setRecognizeOrdinals(boolean withRecognizeOrdinals) { + recognizeOrdinals = withRecognizeOrdinals; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FindValuesOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FindValuesOptions.java new file mode 100644 index 000000000..d45e88816 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FindValuesOptions.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Locale; + +/** + * Contains options used to control how choices are recognized in a users utterance. + */ +public class FindValuesOptions { + @JsonProperty(value = "allowPartialMatches") + private boolean allowPartialMatches; + + @JsonProperty(value = "locale") + private String locale = Locale.ENGLISH.getDisplayName(); + + @JsonProperty(value = "maxTokenDistance") + private int maxTokenDistance = 2; + + @JsonProperty(value = "tokenizer") + private TokenizerFunction tokenizer; + + /** + * Gets value indicating whether only some of the tokens in a value need to exist to be + * considered. + * @return true if only some of the tokens in a value need to exist to be considered; + * otherwise false. + */ + public boolean getAllowPartialMatches() { + return allowPartialMatches; + } + + /** + * Sets value indicating whether only some of the tokens in a value need to exist to be + * considered. + * @param withAllowPartialMatches true if only some of the tokens in a value need to exist + * to be considered; otherwise false. + */ + public void setAllowPartialMatches(boolean withAllowPartialMatches) { + allowPartialMatches = withAllowPartialMatches; + } + + /** + * Gets the locale/culture code of the utterance. The default is `en-US`. This is optional. + * @return The locale/culture code of the utterance. + */ + public String getLocale() { + return locale; + } + + /** + * Sets the locale/culture code of the utterance. The default is `en-US`. This is optional. + * @param withLocale The locale/culture code of the utterance. + */ + public void setLocale(String withLocale) { + locale = withLocale; + } + + /** + * Gets the maximum tokens allowed between two matched tokens in the utterance. So with + * a max distance of 2 the value "second last" would match the utterance "second from the last" + * but it wouldn't match "Wait a second. That's not the last one is it?". + * The default value is "2". + * @return The maximum tokens allowed between two matched tokens in the utterance. + */ + public int getMaxTokenDistance() { + return maxTokenDistance; + } + + /** + * Gets the maximum tokens allowed between two matched tokens in the utterance. So with + * a max distance of 2 the value "second last" would match the utterance "second from the last" + * but it wouldn't match "Wait a second. That's not the last one is it?". + * The default value is "2". + * @param withMaxTokenDistance The maximum tokens allowed between two matched tokens in the + * utterance. + */ + public void setMaxTokenDistance(int withMaxTokenDistance) { + maxTokenDistance = withMaxTokenDistance; + } + + /** + * Gets the tokenizer to use when parsing the utterance and values being recognized. + * @return The tokenizer to use when parsing the utterance and values being recognized. + */ + public TokenizerFunction getTokenizer() { + return tokenizer; + } + + /** + * Sets the tokenizer to use when parsing the utterance and values being recognized. + * @param withTokenizer The tokenizer to use when parsing the utterance and values being + * recognized. + */ + public void setTokenizer(TokenizerFunction withTokenizer) { + tokenizer = withTokenizer; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FoundChoice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FoundChoice.java new file mode 100644 index 000000000..f1e8351f0 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FoundChoice.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a result from matching user input against a list of choices. + */ +public class FoundChoice { + @JsonProperty(value = "value") + private String value; + + @JsonProperty(value = "index") + private int index; + + @JsonProperty(value = "score") + private float score; + + @JsonProperty(value = "synonym") + private String synonym; + + /** + * Gets the value that was matched. + * @return The value that was matched. + */ + public String getValue() { + return value; + } + + /** + * Sets the value that was matched. + * @param withValue The value that was matched. + */ + public void setValue(String withValue) { + value = withValue; + } + + /** + * Gets the index of the value that was matched. + * @return The index of the value that was matched. + */ + public int getIndex() { + return index; + } + + /** + * Sets the index of the value that was matched. + * @param withIndex The index of the value that was matched. + */ + public void setIndex(int withIndex) { + index = withIndex; + } + + /** + * Gets the accuracy with which the value matched the specified portion of the utterance. A + * value of 1.0 would indicate a perfect match. + * @return The accuracy with which the value matched the specified portion of the utterance. + * A value of 1.0 would indicate a perfect match. + */ + public float getScore() { + return score; + } + + /** + * Sets the accuracy with which the value matched the specified portion of the utterance. A + * value of 1.0 would indicate a perfect match. + * @param withScore The accuracy with which the value matched the specified portion of the + * utterance. A value of 1.0 would indicate a perfect match. + */ + public void setScore(float withScore) { + score = withScore; + } + + /** + * Gets the synonym that was matched. This is optional. + * @return The synonym that was matched. + */ + public String getSynonym() { + return synonym; + } + + /** + * Sets the synonym that was matched. This is optional. + * @param withSynonym The synonym that was matched. + */ + public void setSynonym(String withSynonym) { + synonym = withSynonym; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FoundValue.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FoundValue.java new file mode 100644 index 000000000..9bd3430bf --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/FoundValue.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * This class is internal and should not be used. + * Please use FoundChoice instead. + */ +class FoundValue { + @JsonProperty(value = "value") + private String value; + + @JsonProperty(value = "index") + private int index; + + @JsonProperty(value = "score") + private float score; + + /** + * Gets the value that was matched. + * @return The value that was matched. + */ + public String getValue() { + return value; + } + + /** + * Sets the value that was matched. + * @param withValue The value that was matched. + */ + public void setValue(String withValue) { + value = withValue; + } + + /** + * Gets the index of the value that was matched. + * @return The index of the value that was matched. + */ + public int getIndex() { + return index; + } + + /** + * Sets the index of the value that was matched. + * @param withIndex The index of the value that was matched. + */ + public void setIndex(int withIndex) { + index = withIndex; + } + + /** + * Gets the accuracy with which the value matched the specified portion of the utterance. A + * value of 1.0 would indicate a perfect match. + * @return The accuracy with which the value matched the specified portion of the utterance. + * A value of 1.0 would indicate a perfect match. + */ + public float getScore() { + return score; + } + + /** + * Sets the accuracy with which the value matched the specified portion of the utterance. A + * value of 1.0 would indicate a perfect match. + * @param withScore The accuracy with which the value matched the specified portion of the + * utterance. A value of 1.0 would indicate a perfect match. + */ + public void setScore(float withScore) { + score = withScore; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ListStyle.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ListStyle.java new file mode 100644 index 000000000..fbc5316c1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ListStyle.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +/** + * Controls the way that choices for a `ChoicePrompt` or yes/no options for a `ConfirmPrompt` are + * presented to a user. + */ +public enum ListStyle { + /// Don't include any choices for prompt. + NONE, + + /// Automatically select the appropriate style for the current channel. + AUTO, + + /// Add choices to prompt as an inline list. + INLINE, + + /// Add choices to prompt as a numbered list. + LIST, + + /// Add choices to prompt as suggested actions. + SUGGESTED_ACTION, + + /// Add choices to prompt as a HeroCard with buttons. + HEROCARD +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ModelResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ModelResult.java new file mode 100644 index 000000000..ff04b5c28 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/ModelResult.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +/** + * Contains recognition result information. + * + * @param The type of object recognized. + */ +public class ModelResult { + private String text; + private int start; + private int end; + private String typeName; + private T resolution; + + /** + * Gets the substring of the input that was recognized. + * + * @return The substring of the input that was recognized. + */ + public String getText() { + return text; + } + + /** + * Sets the substring of the input that was recognized. + * + * @param withText The substring of the input that was recognized. + */ + public void setText(String withText) { + text = withText; + } + + /** + * Gets the start character position of the recognized substring. + * @return The start character position of the recognized substring. + */ + public int getStart() { + return start; + } + + /** + * Sets the start character position of the recognized substring. + * @param withStart The start character position of the recognized substring. + */ + public void setStart(int withStart) { + start = withStart; + } + + /** + * Gets the end character position of the recognized substring. + * @return The end character position of the recognized substring. + */ + public int getEnd() { + return end; + } + + /** + * Starts the end character position of the recognized substring. + * @param withEnd The end character position of the recognized substring. + */ + public void setEnd(int withEnd) { + end = withEnd; + } + + /** + * Gets the type of entity that was recognized. + * @return The type of entity that was recognized. + */ + public String getTypeName() { + return typeName; + } + + /** + * Sets the type of entity that was recognized. + * @param withTypeName The type of entity that was recognized. + */ + public void setTypeName(String withTypeName) { + typeName = withTypeName; + } + + /** + * Gets the recognized object. + * @return The recognized object. + */ + public T getResolution() { + return resolution; + } + + /** + * Sets the recognized object. + * @param withResolution The recognized object. + */ + public void setResolution(T withResolution) { + resolution = withResolution; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/SortedValue.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/SortedValue.java new file mode 100644 index 000000000..7915ddb05 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/SortedValue.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A value that can be sorted and still refer to its original position with a source array. + */ +public class SortedValue { + @JsonProperty(value = "value") + private String value; + + @JsonProperty(value = "index") + private int index; + + /** + * Creates a sort value. + * @param withValue The value that will be sorted. + * @param withIndex The values original position within its unsorted array. + */ + public SortedValue(String withValue, int withIndex) { + value = withValue; + index = withIndex; + } + + /** + * Gets the value that will be sorted. + * @return The value that will be sorted. + */ + public String getValue() { + return value; + } + + /** + * Sets the value that will be sorted. + * @param withValue The value that will be sorted. + */ + public void setValue(String withValue) { + value = withValue; + } + + /** + * Gets the values original position within its unsorted array. + * @return The values original position within its unsorted array. + */ + public int getIndex() { + return index; + } + + /** + * Sets the values original position within its unsorted array. + * @param withIndex The values original position within its unsorted array. + */ + public void setIndex(int withIndex) { + index = withIndex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Token.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Token.java new file mode 100644 index 000000000..02431564f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Token.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +/** + * Represents an individual token, such as a word in an input string. + */ +public class Token { + private String text; + private int start; + private int end; + private String normalized; + + /** + * Gets the original text of the token. + * + * @return The original text of the token. + */ + public String getText() { + return text; + } + + /** + * Sets the original text of the token. + * + * @param withText The original text of the token. + */ + public void setText(String withText) { + text = withText; + } + + /** + * Appends a string to the text value. + * @param withText The text to append. + */ + public void appendText(String withText) { + if (text != null) { + text += withText; + } else { + text = withText; + } + } + + /** + * Gets the index of the first character of the token within the input. + * @return The index of the first character of the token. + */ + public int getStart() { + return start; + } + + /** + * Sets the index of the first character of the token within the input. + * @param withStart The index of the first character of the token. + */ + public void setStart(int withStart) { + start = withStart; + } + + /** + * Gets the index of the last character of the token within the input. + * @return The index of the last character of the token. + */ + public int getEnd() { + return end; + } + + /** + * Starts the index of the last character of the token within the input. + * @param withEnd The index of the last character of the token. + */ + public void setEnd(int withEnd) { + end = withEnd; + } + + /** + * Gets the normalized version of the token. + * @return A normalized version of the token. + */ + public String getNormalized() { + return normalized; + } + + /** + * Sets the normalized version of the token. + * @param withNormalized A normalized version of the token. + */ + public void setNormalized(String withNormalized) { + normalized = withNormalized; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Tokenizer.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Tokenizer.java new file mode 100644 index 000000000..e7f6c4258 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/Tokenizer.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides the default Tokenizer implementation. + */ +public class Tokenizer implements TokenizerFunction { + + /** + * Simple tokenizer that breaks on spaces and punctuation. The only normalization + * done is to lowercase. + * @param text The input text. + * @param locale Optional, identifies the locale of the input text. + * @return The list of the found Token objects. + */ + @SuppressWarnings("checkstyle:MagicNumber") + @Override + public List tokenize(String text, String locale) { + List tokens = new ArrayList<>(); + Token token = null; + + int length = text == null ? 0 : text.length(); + int i = 0; + + while (i < length) { + int codePoint = text.codePointAt(i); + + String chr = new String(Character.toChars(codePoint)); + + if (isBreakingChar(codePoint)) { + appendToken(tokens, token, i - 1); + token = null; + } else if (codePoint > 0xFFFF) { + appendToken(tokens, token, i - 1); + token = null; + + Token t = new Token(); + t.setStart(i); + t.setEnd(i + chr.length() - 1); + t.setText(chr); + t.setNormalized(chr); + + tokens.add(t); + } else if (token == null) { + token = new Token(); + token.setStart(i); + token.setText(chr); + } else { + token.appendText(chr); + } + + i += chr.length(); + } + + appendToken(tokens, token, length - 1); + return tokens; + } + + private void appendToken(List tokens, Token token, int end) { + if (token != null) { + token.setEnd(end); + token.setNormalized(token.getText().toLowerCase()); + tokens.add(token); + } + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static boolean isBreakingChar(int codePoint) { + return isBetween(codePoint, 0x0000, 0x002F) + || isBetween(codePoint, 0x003A, 0x0040) + || isBetween(codePoint, 0x005B, 0x0060) + || isBetween(codePoint, 0x007B, 0x00BF) + || isBetween(codePoint, 0x02B9, 0x036F) + || isBetween(codePoint, 0x2000, 0x2BFF) + || isBetween(codePoint, 0x2E00, 0x2E7F); + } + + private static boolean isBetween(int value, int from, int to) { + return value >= from && value <= to; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/TokenizerFunction.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/TokenizerFunction.java new file mode 100644 index 000000000..3fb3c459d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/TokenizerFunction.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.choices; + +import java.util.List; + +/** + * Represents a callback method that can break a string into its component tokens. + */ +@FunctionalInterface +public interface TokenizerFunction { + + /** + * The callback method that can break a string into its component tokens. + * @param text The input text. + * @param locale Optional, identifies the locale of the input text. + * @return The list of the found Token objects. + */ + List tokenize(String text, String locale); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/package-info.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/package-info.java new file mode 100644 index 000000000..0adf53345 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/choices/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.dialogs.choices. + */ +package com.microsoft.bot.dialogs.choices; diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/ComponentMemoryScopes.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/ComponentMemoryScopes.java new file mode 100644 index 000000000..f7e1fe0b8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/ComponentMemoryScopes.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory; + +import com.microsoft.bot.dialogs.memory.scopes.MemoryScope; + +/** + * Defines Component Memory Scopes interface for enumerating memory scopes. + */ +public interface ComponentMemoryScopes { + + /** + * Gets the memory scopes. + * + * @return A reference with the memory scopes. + */ + Iterable getMemoryScopes(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/ComponentPathResolvers.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/ComponentPathResolvers.java new file mode 100644 index 000000000..057a90629 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/ComponentPathResolvers.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory; +/** + * Interface for declaring path resolvers in the memory system. + */ +public interface ComponentPathResolvers { + /** + * Return enumeration of pathresolvers. + * + * @return collection of PathResolvers. + */ + Iterable getPathResolvers(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/DialogStateManager.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/DialogStateManager.java new file mode 100644 index 000000000..135e3289d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/DialogStateManager.java @@ -0,0 +1,858 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.ComponentRegistration; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogPath; +import com.microsoft.bot.dialogs.DialogsComponentRegistration; +import com.microsoft.bot.dialogs.ObjectPath; +import com.microsoft.bot.dialogs.memory.scopes.MemoryScope; +import com.microsoft.bot.schema.ResultPair; + +import org.apache.commons.lang3.StringUtils; + +/** + * The DialogStateManager manages memory scopes and pathresolvers MemoryScopes + * are named root level Objects, which can exist either in the dialogcontext or + * off of turn state PathResolvers allow for shortcut behavior for mapping + * things like $foo to dialog.foo. + */ +public class DialogStateManager implements Map { + + /** + * Information for tracking when path was last modified. + */ + private final String pathTracker = "dialog._tracker.paths"; + + private static final char[] SEPARATORS = {',', '[' }; + + private final DialogContext dialogContext; + private int version; + + private ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + /** + * Initializes a new instance of the + * {@link com.microsoft.bot.dialogs.memory.DialogStateManager} class. + * + * @param dc The dialog context for the current turn of the + * conversation. + */ + public DialogStateManager(DialogContext dc) { + this(dc, null); + } + + /** + * Initializes a new instance of the + * {@link com.microsoft.bot.dialogs.memory.DialogStateManager} class. + * + * @param dc The dialog context for the current turn of the + * conversation. + * @param configuration Configuration for the dialog state manager. + */ + public DialogStateManager(DialogContext dc, DialogStateManagerConfiguration configuration) { + ComponentRegistration.add(new DialogsComponentRegistration()); + + if (dc != null) { + dialogContext = dc; + } else { + throw new IllegalArgumentException("dc cannot be null."); + } + + if (configuration != null) { + this.configuration = configuration; + } else { + this.configuration = dc.getContext().getTurnState().get(DialogStateManagerConfiguration.class.getName()); + } + + if (this.configuration == null) { + this.configuration = new DialogStateManagerConfiguration(); + + Map turnStateServices = dc.getContext().getTurnState().getTurnStateServices(); + for (Map.Entry entry : turnStateServices.entrySet()) { + if (entry.getValue() instanceof MemoryScope[]) { + this.configuration.getMemoryScopes().addAll(Arrays.asList((MemoryScope[]) entry.getValue())); + } + if (entry.getValue() instanceof PathResolver[]) { + this.configuration.getPathResolvers().addAll(Arrays.asList((PathResolver[]) entry.getValue())); + } + } + + Iterable components = ComponentRegistration.getComponents(); + + components.forEach((component) -> { + if (component instanceof ComponentMemoryScopes) { + ((ComponentMemoryScopes) component).getMemoryScopes().forEach((scope) -> { + this.configuration.getMemoryScopes().add(scope); + }); + } + if (component instanceof ComponentPathResolvers) { + ((ComponentPathResolvers) component).getPathResolvers().forEach((pathResolver) -> { + this.configuration.getPathResolvers().add(pathResolver); + }); + } + }); + } + // cache for any other new dialogStatemanager instances in this turn. + dc.getContext().getTurnState().replace(this.configuration); + } + + private DialogStateManagerConfiguration configuration; + + /** + * Sets the configured path resolvers and memory scopes for the dialog. + * + * @return The DialogStateManagerConfiguration. + */ + public DialogStateManagerConfiguration getConfiguration() { + return configuration; + } + + /** + * Sets the configured path resolvers and memory scopes for the dialog. + * + * @param withDialogStateManagerConfiguration The configuration to set. + */ + public void setConfiguration(DialogStateManagerConfiguration withDialogStateManagerConfiguration) { + this.configuration = withDialogStateManagerConfiguration; + } + + /** + * Gets a value indicating whether the dialog state manager is read-only. + * + * @return true + */ + public Boolean getIsReadOnly() { + return true; + } + + /** + * Gets the elements with the specified key. + * + * @param key Key to get or set the element. + * @return The element with the specified key. + */ + public Object getElement(String key) { + // return GetValue(key); + return null; + } + + /** + * Sets the elements with the specified key. + * + * @param key Key to get or set the element. + * @param element The element to store with the provided key. + */ + public void setElement(String key, Object element) { + if (key.indexOf(SEPARATORS[0]) == -1 && key.indexOf(SEPARATORS[1]) == -1) { + MemoryScope scope = getMemoryScope(key); + if (scope != null) { + try { + scope.setMemory(dialogContext, mapper.writeValueAsString(element)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } else { + throw new IllegalArgumentException(getBadScopeMessage(key)); + } + } + } + + /** + * Get MemoryScope by name. + * + * @param name Name of scope. + * @return A memory scope. + */ + public MemoryScope getMemoryScope(String name) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null."); + } + Optional result = configuration.getMemoryScopes().stream() + .filter((scope) -> scope.getName().equalsIgnoreCase(name)) + .findFirst(); + return result.isPresent() ? result.get() : null; + } + + /** + * Version help caller to identify the updates and decide cache or not. + * + * @return Current version + */ + public String version() { + return Integer.toString(version); + } + + /** + * ResolveMemoryScope will find the MemoryScope for and return the remaining + * path. + * + * @param path Incoming path to resolve to scope and remaining path. + * @param remainingPath Remaining subpath in scope. + * @return The memory scope. + */ + public MemoryScope resolveMemoryScope(String path, StringBuilder remainingPath) { + String scope = path; + int sepIndex = -1; + int dot = StringUtils.indexOfIgnoreCase(path, "."); + int openSquareBracket = StringUtils.indexOfIgnoreCase(path, "["); + + if (dot > 0 && openSquareBracket > 0) { + sepIndex = Math.min(dot, openSquareBracket); + } else if (dot > 0) { + sepIndex = dot; + } else if (openSquareBracket > 0) { + sepIndex = openSquareBracket; + } + + if (sepIndex > 0) { + scope = path.substring(0, sepIndex); + MemoryScope memoryScope = getMemoryScope(scope); + if (memoryScope != null) { + remainingPath.append(path.substring(sepIndex + 1)); + return memoryScope; + } + } + + MemoryScope resultScope = getMemoryScope(scope); + if (resultScope == null) { + throw new IllegalArgumentException(getBadScopeMessage(path)); + } else { + return resultScope; + } + } + + /** + * Transform the path using the registered PathTransformers. + * + * @param path Path to transform. + * @return The transformed path. + */ + public String transformPath(String path) { + List resolvers = configuration.getPathResolvers(); + + for (PathResolver resolver : resolvers) { + path = resolver.transformPath(path); + } + + return path; + } + + /** + * Get the value from memory using path expression (NOTE: This always returns + * clone of value). + * + * @param the value type to return. + * @param path path expression to use. + * @param clsType the Type that is being requested as a result + * @return ResultPair with boolean and requested type TypeT as a result + */ + public ResultPair tryGetValue(String path, Class clsType) { + TypeT instance = null; + + if (path == null) { + throw new IllegalArgumentException("path cannot be null"); + } + + path = transformPath(path); + + MemoryScope memoryScope = null; + StringBuilder remainingPath = new StringBuilder(); + + try { + memoryScope = resolveMemoryScope(path, remainingPath); + } catch (Exception err) { + // Trace.TraceError(err.Message); + return new ResultPair<>(false, instance); + } + + if (memoryScope == null) { + return new ResultPair<>(false, instance); + } + + if (remainingPath.length() == 0) { + Object memory = memoryScope.getMemory(dialogContext); + if (memory == null) { + return new ResultPair<>(false, instance); + } + + instance = (TypeT) ObjectPath.mapValueTo(memory, clsType); + + return new ResultPair<>(true, instance); + } + + // HACK to support .First() retrieval on turn.recognized.entities.foo, + // replace with Expressions + // once expression ship + final String first = ".FIRST()"; + int iFirst = path.toUpperCase(Locale.US).lastIndexOf(first); + if (iFirst >= 0) { + remainingPath = new StringBuilder(path.substring(iFirst + first.length())); + path = path.substring(0, iFirst); + ResultPair getResult = tryGetFirstNestedValue(new AtomicReference(path), this); + if (getResult.result()) { + if (StringUtils.isEmpty(remainingPath.toString())) { + instance = (TypeT) ObjectPath.mapValueTo(getResult.getRight(), clsType); + return new ResultPair<>(true, instance); + } + instance = (TypeT) ObjectPath.tryGetPathValue(getResult.getRight(), remainingPath.toString(), clsType); + + return new ResultPair<>(true, instance); + } + + return new ResultPair<>(false, instance); + } + + instance = (TypeT) ObjectPath.tryGetPathValue(this, path, clsType); + + return new ResultPair<>(instance != null, instance); + } + + /** + * Get the value from memory using path expression (NOTE: This always returns + * clone of value). + * + * @param The value type to return. + * @param pathExpression Path expression to use. + * @param defaultValue Default value to return if there is none found. + * @param clsType Type of value that is being requested as a return. + * @return Result or the default value if the path is not valid. + */ + public T getValue(String pathExpression, T defaultValue, Class clsType) { + if (pathExpression == null) { + throw new IllegalArgumentException("path cannot be null"); + } + + ResultPair result = tryGetValue(pathExpression, clsType); + if (result.result()) { + return result.value(); + } else { + return defaultValue; + } + } + + /** + * Get a int value from memory using a path expression. + * + * @param pathExpression Path expression. + * @param defaultValue Default value if the value doesn't exist. + * @return Value or default value if path is not valid. + */ + public int getIntValue(String pathExpression, int defaultValue) { + if (pathExpression == null) { + throw new IllegalArgumentException("path cannot be null"); + } + + ResultPair result = tryGetValue(pathExpression, Integer.class); + if (result.result()) { + return result.value(); + } else { + return defaultValue; + } + } + + /** + * Get a boolean value from memory using a path expression. + * + * @param pathExpression Path expression. + * @param defaultValue Default value if the value doesn't exist. + * @return Value or default value if path is not valid. + */ + public Boolean getBoolValue(String pathExpression, Boolean defaultValue) { + if (pathExpression == null || StringUtils.isEmpty(pathExpression)) { + throw new IllegalArgumentException("path cannot be null"); + } + + ResultPair result = tryGetValue(pathExpression, Boolean.class); + if (result.result()) { + return result.value(); + } else { + return defaultValue; + } + } + + /** + * Get a String value from memory using a path expression. + * + * @param pathExpression The path expression. + * @param defaultValue Default value if the value doesn't exist. + * @return String or default value if path is not valid. + */ + public String getStringValue(String pathExpression, String defaultValue) { + return getValue(pathExpression, defaultValue, String.class); + } + + /** + * Set memory to value. + * + * @param path Path to memory. + * @param value Object to set. + */ + public void setValue(String path, Object value) { + if (value instanceof CompletableFuture) { + throw new IllegalArgumentException( + String.format("%s = You can't pass an unresolved CompletableFuture to SetValue", path)); + } + + if (path == null || StringUtils.isEmpty(path)) { + throw new IllegalArgumentException("path cannot be null"); + } + + if (value != null) { + value = mapper.valueToTree(value); + } + + path = transformPath(path); + if (trackChange(path, value)) { + ObjectPath.setPathValue(this, path, value); + } + + // Every set will increase version + version++; + } + + /** + * Remove property from memory. + * + * @param path Path to remove the leaf property. + */ + public void removeValue(String path) { + if (!StringUtils.isNotBlank(path)) { + throw new IllegalArgumentException("Path cannot be null"); + } + + path = transformPath(path); + if (trackChange(path, null)) { + ObjectPath.removePathValue(this, path); + } + } + + /** + * Gets all memoryscopes suitable for logging. + * + * @return JsonNode that which represents all memory scopes. + */ + public JsonNode getMemorySnapshot() { + ObjectNode result = mapper.createObjectNode(); + + List scopes = configuration.getMemoryScopes().stream().filter((x) -> x.getIncludeInSnapshot()) + .collect(Collectors.toList()); + for (MemoryScope scope : scopes) { + Object memory = scope.getMemory(dialogContext); + if (memory != null) { + try { + result.put(scope.getName(), mapper.writeValueAsString(memory)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + } + return result; + } + + /** + * Load all of the scopes. + * + * @return A Completed Future. + */ + public CompletableFuture loadAllScopes() { + configuration.getMemoryScopes().forEach((scope) -> { + scope.load(dialogContext, false).join(); + }); + return CompletableFuture.completedFuture(null); + } + + /** + * Save all changes for all scopes. + * + * @return Completed Future + */ + public CompletableFuture saveAllChanges() { + configuration.getMemoryScopes().forEach((memoryScope) -> { + memoryScope.saveChanges(dialogContext, false).join(); + }); + return CompletableFuture.completedFuture(null); + } + + /** + * Delete the memory for a scope. + * + * @param name name of the scope + * @return Completed CompletableFuture + */ + public CompletableFuture deleteScopesMemory(String name) { + // Make a copy here that is final so it can be used in lamdba expression below + final String uCaseName = name.toUpperCase(); + MemoryScope scope = configuration.getMemoryScopes().stream().filter((s) -> { + return s.getName().toUpperCase() == uCaseName; + }).findFirst().get(); + if (scope != null) { + return scope.delete(dialogContext).thenApply(result -> null); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Adds an element to the dialog state manager. + * + * @param key Key of the element to add. + * @param value Value of the element to add. + */ + public void add(String key, Object value) { + throw new UnsupportedOperationException(); + } + + /** + * Determines whether the dialog state manager contains an element with the + * specified key. + * + * @param key The key to locate in the dialog state manager. + * @return true if the dialog state manager contains an element with the key; + * otherwise, false. + */ + public Boolean containsKey(String key) { + for (MemoryScope scope : configuration.getMemoryScopes()) { + if (scope.getName().toUpperCase().equals(key.toUpperCase())) { + return true; + } + } + return false; + } + + /** + * Removes the element with the specified key from the dialog state manager. + * + * @param key The key of the element to remove. + * @return true if the element is succesfully removed; otherwise, false. + */ + public Boolean remove(String key) { + throw new UnsupportedOperationException(); + } + + /** + * Gets the value associated with the specified key. + * + * @param key The key whose value to get. + * @param value When this method returns, the value associated with the + * specified key, if the key is found; otherwise, the default value + * for the type of the value parameter. + * @return true if the dialog state manager contains an element with the + * specified key; + */ + public ResultPair tryGetValue(String key, Object value) { + return tryGetValue(key, Object.class); + } + + /** + * Adds an item to the dialog state manager. + * + * @param item The SimpleEntry with the key and Object of the item to add. + */ + public void add(SimpleEntry item) { + throw new UnsupportedOperationException(); + } + + /** + * Removes all items from the dialog state manager. + */ + public void clear() { + throw new UnsupportedOperationException(); + } + + /** + * Determines whether the dialog state manager contains a specific value. + * + * @param item The of the item to locate. + * @return True if item is found in the dialog state manager; otherwise,false + */ + public Boolean contains(SimpleEntry item) { + throw new UnsupportedOperationException(); + } + + /** + * Copies the elements of the dialog state manager to an array starting at a + * particular index. + * + * @param array The one-dimensional array that is the destination of the + * elements copiedfrom the dialog state manager. The array + * must have zero-based indexing. + * @param arrayIndex The zero-based index in array at which copying begins. + */ + public void copyTo(SimpleEntry[] array, int arrayIndex) { + for (MemoryScope scope : configuration.getMemoryScopes()) { + array[arrayIndex++] = new SimpleEntry(scope.getName(), scope.getMemory(dialogContext)); + } + } + + /// + /// Removes the first occurrence of a specific Object from the dialog state + /// manager. + /// + /// The Object to remove from the dialog state + /// manager. + /// true if the item was successfully removed from the dialog + /// state manager; + /// otherwise, false. + /// This method is not supported. + /** + * Removes the first occurrence of a specific Object from the dialog state + * manager. + * + * @param item The Object to remove from the dialog state manager. + * @return true if the item was successfully removed from the dialog state + * manager otherwise false + */ + public boolean remove(SimpleEntry item) { + throw new UnsupportedOperationException(); + } + + /** + * Returns an Iterator that iterates through the collection. + * + * @return An Iterator that can be used to iterate through the collection. + */ + public Iterable> getEnumerator() { + List> resultList = new ArrayList>(); + for (MemoryScope scope : configuration.getMemoryScopes()) { + resultList.add(new SimpleEntry(scope.getName(), scope.getMemory(dialogContext))); + } + return resultList; + } + + /** + * Track when specific paths are changed. + * + * @param paths Paths to track. + * @return Normalized paths to pass to AnyPathChanged. + */ + public List trackPaths(Iterable paths) { + List allPaths = new ArrayList(); + for (String path : paths) { + String tpath = transformPath(path); + // Track any path that resolves to a constant path + ArrayList resolved = ObjectPath.tryResolvePath(this, tpath); + String[] segments = resolved.toArray(new String[resolved.size()]); + if (resolved != null) { + String npath = String.join("_", segments); + setValue(pathTracker + "." + npath, 0); + allPaths.add(npath); + } + } + return allPaths; + } + + /** + * Check to see if any path has changed since watermark. + * + * @param counter Time counter to compare to. + * @param paths Paths from Trackpaths to check. + * @return True if any path has changed since counter. + */ + public Boolean anyPathChanged(int counter, Iterable paths) { + Boolean found = false; + if (paths != null) { + for (String path : paths) { + int resultValue = getValue(pathTracker + "." + path, -1, Integer.class); + if (resultValue != -1 && resultValue > counter) { + found = true; + break; + } + } + } + return found; + } + + @SuppressWarnings("PMD.UnusedFormalParameter") + private static ResultPair tryGetFirstNestedValue(AtomicReference remainingPath, Object memory) { + ArrayNode array = new ArrayNode(JsonNodeFactory.instance); + Object value; + array = ObjectPath.tryGetPathValue(memory, remainingPath.get(), ArrayNode.class); + + if (array != null && array.size() > 0) { + JsonNode firstNode = array.get(0); + if (firstNode instanceof ArrayNode) { + if (firstNode.size() > 0) { + JsonNode secondNode = firstNode.get(0); + value = ObjectPath.mapValueTo(secondNode, Object.class); + return new ResultPair(true, value); + } + return new ResultPair(false, null); + } + value = ObjectPath.mapValueTo(firstNode, Object.class); + return new ResultPair(true, value); + } + return new ResultPair(false, null); + } + + private String getBadScopeMessage(String path) { + StringBuilder errorMessage = new StringBuilder(path); + errorMessage.append(" does not match memory scopes:["); + List scopeNames = new ArrayList(); + List scopes = configuration.getMemoryScopes(); + scopes.forEach((sc) -> { + scopeNames.add(sc.getName()); + }); + errorMessage.append(String.join(",", scopeNames)); + errorMessage.append("]"); + return errorMessage.toString(); + } + + private Boolean trackChange(String path, Object value) { + Boolean hasPath = false; + ArrayList segments = ObjectPath.tryResolvePath(this, path, false); + if (segments != null) { + String root = segments.size() > 1 ? (String) segments.get(1) : new String(); + + // Skip _* as first scope, i.e. _adaptive, _tracker, ... + if (!root.startsWith("_")) { + List stringSegments = segments.stream().map(Object -> Objects.toString(Object, null)) + .collect(Collectors.toList()); + + // Convert to a simple path with _ between segments + String pathName = String.join("_", stringSegments); + String trackedPath = String.format("%s.%s", pathTracker, pathName); + Integer counter = null; + /** + * + */ + ResultPair result = tryGetValue(trackedPath, Integer.class); + if (result.result()) { + if (counter == null) { + counter = getValue(DialogPath.EVENTCOUNTER, 0, Integer.class); + } + setValue(trackedPath, counter); + } + if (value instanceof Map) { + final int count = counter; + ((Map) value).forEach((key, val) -> { + checkChildren(key, val, trackedPath, count); + }); + } else if (value instanceof ObjectNode) { + ObjectNode node = (ObjectNode) value; + Iterator fields = node.fieldNames(); + + while (fields.hasNext()) { + String field = fields.next(); + checkChildren(field, node.findValue(field), trackedPath, counter); + } + } + } + hasPath = true; + } + return hasPath; + } + + private void checkChildren(String property, Object instance, String path, Integer counter) { + // Add new child segment + String trackedPath = path + "_" + property.toLowerCase(); + ResultPair pathCheck = tryGetValue(trackedPath, Integer.class); + if (pathCheck.result()) { + if (counter == null) { + counter = getValue(DialogPath.EVENTCOUNTER, 0, Integer.class); + } + setValue(trackedPath, counter); + } + + if (instance instanceof Map) { + final int count = counter; + ((Map) instance).forEach((key, value) -> { + checkChildren(key, value, trackedPath, count); + }); + } else if (instance instanceof ObjectNode) { + ObjectNode node = (ObjectNode) instance; + Iterator fields = node.fieldNames(); + + while (fields.hasNext()) { + String field = fields.next(); + checkChildren(field, node.findValue(field), trackedPath, counter); + } + } + } + + @Override + public final int size() { + return configuration.getMemoryScopes().size(); + } + + @Override + public final boolean isEmpty() { + return size() == 0; + } + + @Override + public final boolean containsKey(Object key) { + return false; + } + + @Override + public final boolean containsValue(Object value) { + return false; + } + + @Override + public final Object get(Object key) { + return tryGetValue(key.toString(), Object.class).value(); + } + + @Override + public final Object put(String key, Object value) { + setElement(key, value); + return value; + } + + @Override + public final Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public final void putAll(Map m) { + } + + @Override + public final Set keySet() { + return configuration.getMemoryScopes().stream().map(scope -> scope.getName()).collect(Collectors.toSet()); + } + + @Override + public final Collection values() { + return configuration.getMemoryScopes().stream().map(scope -> scope.getMemory(dialogContext)) + .collect(Collectors.toSet()); + } + + @Override + public final Set> entrySet() { + Set> resultSet = new HashSet>(); + configuration.getMemoryScopes().forEach((scope) -> { + resultSet.add(new AbstractMap.SimpleEntry(scope.getName(), scope.getMemory(dialogContext))); + }); + + return resultSet; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/DialogStateManagerConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/DialogStateManagerConfiguration.java new file mode 100644 index 000000000..fd7bc2a71 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/DialogStateManagerConfiguration.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory; + +import java.util.ArrayList; +import java.util.List; + +import com.microsoft.bot.dialogs.memory.scopes.MemoryScope; + +/** + * Configures the path resolvers and memory scopes for the dialog state manager. + */ +public class DialogStateManagerConfiguration { + + private List pathResolvers = new ArrayList(); + + private List memoryScopes = new ArrayList(); + + + /** + * @return Returns the list of PathResolvers. + */ + public List getPathResolvers() { + return this.pathResolvers; + } + + + /** + * @param withPathResolvers Set the list of PathResolvers. + */ + public void setPathResolvers(List withPathResolvers) { + this.pathResolvers = withPathResolvers; + } + + + /** + * @return Returns the list of MemoryScopes. + */ + public List getMemoryScopes() { + return this.memoryScopes; + } + + + /** + * @param withMemoryScopes Set the list of MemoryScopes. + */ + public void setMemoryScopes(List withMemoryScopes) { + this.memoryScopes = withMemoryScopes; + } + + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/PathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/PathResolver.java new file mode 100644 index 000000000..8a7c6c7e8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/PathResolver.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory; +/** + * Defines Path Resolver interface for transforming paths. + */ +public interface PathResolver { + /** + * + * @param path path to inspect. + * @return transformed path. + */ + String transformPath(String path); + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/package-info.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/package-info.java new file mode 100644 index 000000000..e4fbf9cfe --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.dialogs.memory. + */ +package com.microsoft.bot.dialogs.memory; diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AliasPathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AliasPathResolver.java new file mode 100644 index 000000000..f5ff47df8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AliasPathResolver.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.pathresolvers; + +import com.microsoft.bot.dialogs.memory.PathResolver; + +/** + * Maps aliasXXX to path.xxx ($foo to dialog.foo). + */ +public class AliasPathResolver implements PathResolver { + + private final String postfix; + private final String prefix; + + /** + * + * @param alias Alias name. + * @param prefix Prefix name. + * @param postfix Postfix name. + */ + public AliasPathResolver(String alias, String prefix, String postfix) { + if (alias == null) { + throw new IllegalArgumentException("alias cannot be null"); + } + + if (prefix == null) { + throw new IllegalArgumentException("prefix cannot be null."); + } + + this.prefix = prefix.trim(); + + setAlias(alias.trim()); + + if (postfix == null) { + this.postfix = ""; + } else { + this.postfix = postfix; + } + } + + /** + * @return Gets the alias name. + */ + public String getAlias() { + return this.alias; + } + + /** + * @param alias Sets the alias name. + */ + private void setAlias(String alias) { + this.alias = alias; + } + + /** + * The alias name. + */ + private String alias; + + /** + * @param path Path to transform. + * @return The transformed path. + */ + public String transformPath(String path) { + if (path == null) { + throw new IllegalArgumentException("path cannot be null."); + } + + path = path.trim(); + if (path.startsWith(getAlias()) && path.length() > getAlias().length() + && isPathChar(path.charAt(getAlias().length()))) { + // here we only deals with trailing alias, alias in middle be handled in further + // breakdown + // $xxx -> path.xxx + return prefix + path.substring(getAlias().length()) + postfix; + } + + return path; + } + + /** + * + * @param ch Character to verify. + * @return true if the character is valid for a path; otherwise, false. + */ + protected Boolean isPathChar(char ch) { + return Character.isLetter(ch) || ch == '_'; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AtAtPathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AtAtPathResolver.java new file mode 100644 index 000000000..7c60062ee --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AtAtPathResolver.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.pathresolvers; + +/** + * Maps @@ to turn.recognized.entitites.xxx array. + */ +public class AtAtPathResolver extends AliasPathResolver { + + /** + * Initializes a new instance of the AtAtPathResolver class. + */ + public AtAtPathResolver() { + super("@@", "turn.recognized.entities.", null); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AtPathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AtPathResolver.java new file mode 100644 index 000000000..553b28008 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/AtPathResolver.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.pathresolvers; + +/** + * Maps @@ to turn.recognized.entitites.xxx array. + */ +public class AtPathResolver extends AliasPathResolver { + + private final String prefix = "turn.recognized.entities."; + + private static final char[] DELIMS = {'.', '[' }; + + /** + * Initializes a new instance of the AtPathResolver class. + */ + public AtPathResolver() { + super("@", "", null); + } + + /** + * Transforms the path. + * + * @param path Path to transform. + * @return The transformed path. + */ + @Override + public String transformPath(String path) { + if (path == null) { + throw new IllegalArgumentException("path cannot be null."); + } + + path = path.trim(); + if (path.startsWith("@") && path.length() > 1 && isPathChar(path.charAt(1))) { + int end = 0; + int endperiod = path.indexOf(DELIMS[0]); + int endbracket = path.indexOf(DELIMS[1]); + if (endperiod == -1 && endbracket == -1) { + end = path.length(); + } else { + end = Math.max(endperiod, endbracket); + } + + String property = path.substring(1, end); + String suffix = path.substring(end); + path = prefix + property + ".first()" + suffix; + } + + return path; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/DollarPathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/DollarPathResolver.java new file mode 100644 index 000000000..ae455db19 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/DollarPathResolver.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.pathresolvers; + +/** + * Resolve $xxx. + */ +public class DollarPathResolver extends AliasPathResolver { + + /** + * Initializes a new instance of the DollarPathResolver class. + */ + public DollarPathResolver() { + super("$", "dialog.", null); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/HashPathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/HashPathResolver.java new file mode 100644 index 000000000..8f04f13bc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/HashPathResolver.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.pathresolvers; + +/** + * Maps #xxx to turn.recognized.intents.xxx. + */ +public class HashPathResolver extends AliasPathResolver { + + /** + * Initializes a new instance of the HashPathResolver class. + */ + public HashPathResolver() { + super("#", "turn.recognized.intents.", null); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/PercentPathResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/PercentPathResolver.java new file mode 100644 index 000000000..03f5bb9d8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/PercentPathResolver.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.pathresolvers; + +/** + * Maps %xxx to settings.xxx (aka activeDialog.Instance.xxx). + */ +public class PercentPathResolver extends AliasPathResolver { + + /** + * Initializes a new instance of the PercentPathResolver class. + */ + public PercentPathResolver() { + super("%", "class.", null); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/package-info.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/package-info.java new file mode 100644 index 000000000..e9844e3b7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/pathresolvers/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.dialogs.memory.pathresolvers. + */ +package com.microsoft.bot.dialogs.memory.pathresolvers; diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/BotStateMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/BotStateMemoryScope.java new file mode 100644 index 000000000..fbf2224cf --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/BotStateMemoryScope.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.BotState; +import com.microsoft.bot.builder.BotState.CachedBotState; +import com.microsoft.bot.dialogs.DialogContext; + +/** + * BotStateMemoryScope represents a BotState scoped memory. + * + * @param The BotState type. + */ +public class BotStateMemoryScope extends MemoryScope { + + private Class type; + + /** + * Initializes a new instance of the TurnMemoryScope class. + * + * @param type The Type of T that is being created. + * @param name Name of the property. + */ + public BotStateMemoryScope(Class type, String name) { + super(name, true); + this.type = type; + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + T botState = getBotState(dialogContext); + if (botState != null) { + CachedBotState cachedState = botState.getCachedState(dialogContext.getContext()); + return cachedState.getState(); + } else { + return null; + } + } + + /** + * Changes the backing Object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + throw new UnsupportedOperationException("You cannot replace the root BotState Object."); + } + + /** + * + */ + @Override + public CompletableFuture load(DialogContext dialogContext, Boolean force) { + T botState = getBotState(dialogContext); + + if (botState != null) { + return botState.load(dialogContext.getContext(), force); + } else { + return CompletableFuture.completedFuture(null); + } + } + + /** + * @param dialogContext + * @param force + * @return A future that represents the + */ + @Override + public CompletableFuture saveChanges(DialogContext dialogContext, Boolean force) { + T botState = getBotState(dialogContext); + + if (botState != null) { + return botState.saveChanges(dialogContext.getContext(), force); + } else { + return CompletableFuture.completedFuture(null); + } + } + + private T getBotState(DialogContext dialogContext) { + return dialogContext.getContext().getTurnState().get(type); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ClassMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ClassMemoryScope.java new file mode 100644 index 000000000..9df3664d2 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ClassMemoryScope.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * MemoryScope represents a named memory scope abstract class. + */ +public class ClassMemoryScope extends MemoryScope { + /** + * Initializes a new instance of the TurnMemoryScope class. + */ + public ClassMemoryScope() { + super(ScopePath.CLASS, false); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + // if active dialog is a container dialog then "dialog" binds to it. + if (dialogContext.getActiveDialog() != null) { + Dialog dialog = dialogContext.findDialog(dialogContext.getActiveDialog().getId()); + if (dialog != null) { + return new ReadOnlyObject(dialog); + } + } + return null; +} + + /** + * Changes the backing Object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + throw new UnsupportedOperationException("You can't modify the class scope."); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ConversationMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ConversationMemoryScope.java new file mode 100644 index 000000000..e32031dc5 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ConversationMemoryScope.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * MemoryScope represents a named memory scope abstract class. + */ +public class ConversationMemoryScope extends BotStateMemoryScope { + /** + * DialogMemoryScope maps "this" to dc.ActiveDialog.State. + */ + public ConversationMemoryScope() { + super(ConversationState.class, ScopePath.CONVERSATION); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogClassMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogClassMemoryScope.java new file mode 100644 index 000000000..aee1efad4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogClassMemoryScope.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.microsoft.bot.dialogs.DialogContainer; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * Initializes a new instance of the DialogClassMemoryScope class. + */ +public class DialogClassMemoryScope extends MemoryScope { + /** + * Initializes a new instance of the DialogClassMemoryScope class. + */ + public DialogClassMemoryScope() { + super(ScopePath.DIALOG_CLASS, false); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + // if active dialog is a container dialog then "dialogclass" binds to it. + if (dialogContext.getActiveDialog() != null) { + Object dialog = dialogContext.findDialog(dialogContext.getActiveDialog().getId()); + if (dialog instanceof DialogContainer) { + return new ReadOnlyObject(dialog); + } + } + + // Otherwise we always bind to parent, or if there is no parent the active + // dialog + if (dialogContext.getParent() != null && dialogContext.getParent().getActiveDialog() != null) { + return new ReadOnlyObject(dialogContext.findDialog(dialogContext.getParent().getActiveDialog().getId())); + } else if (dialogContext.getActiveDialog() != null) { + return new ReadOnlyObject(dialogContext.findDialog(dialogContext.getActiveDialog().getId())); + } else { + return null; + } + + } + + /** + * Changes the backing Object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + throw new UnsupportedOperationException("You can't modify the dialogclass scope"); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogContextMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogContextMemoryScope.java new file mode 100644 index 000000000..e665ef042 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogContextMemoryScope.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.schema.Serialization; +import java.util.Optional; + +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogInstance; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * DialogContextMemoryScope maps "dialogcontext" to properties. + */ +public class DialogContextMemoryScope extends MemoryScope { + + private final String stackKey = "stack"; + + private final String activeDialogKey = "activeDialog"; + + private final String parentKey = "parent"; + + /** + * Initializes a new instance of the TurnMemoryScope class. + */ + public DialogContextMemoryScope() { + super(ScopePath.DIALOG_CONTEXT, false); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + ObjectNode memory = Serialization.createObjectNode(); + ArrayNode stack = Serialization.createArrayNode(); + DialogContext currentDc = dialogContext; + + // go to leaf node + while (currentDc.getChild() != null) { + currentDc = currentDc.getChild(); + } + + while (currentDc != null) { + // (PORTERS NOTE: javascript stack is reversed with top of stack on end) + currentDc.getStack().forEach(item -> { + if (item.getId().startsWith("ActionScope[")) { + stack.add(item.getId()); + } + + }); + + currentDc = currentDc.getParent(); + } + + // top of stack is stack[0]. + memory.set(stackKey, stack); + memory.put(activeDialogKey, Optional.ofNullable(dialogContext) + .map(DialogContext::getActiveDialog) + .map(DialogInstance::getId) + .orElse(null)); + memory.put(parentKey, Optional.ofNullable(dialogContext) + .map(DialogContext::getParent) + .map(DialogContext::getActiveDialog) + .map(DialogInstance::getId) + .orElse(null)); + return memory; + } + + /** + * Changes the backing Object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + throw new UnsupportedOperationException("You can't modify the dialogcontext scope"); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogMemoryScope.java new file mode 100644 index 000000000..7c03e0e9e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/DialogMemoryScope.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import java.util.Map; + +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContainer; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * DialogMemoryScope maps "dialog" to dc.Parent?.ActiveDialog.State ?? ActiveDialog.State. + */ +public class DialogMemoryScope extends MemoryScope { + /** + * Initializes a new instance of the TurnMemoryScope class. + */ + public DialogMemoryScope() { + super(ScopePath.DIALOG, true); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + if (dialogContext.getActiveDialog() != null) { + Dialog dialog = dialogContext.findDialog(dialogContext.getActiveDialog().getId()); + if (dialog instanceof DialogContainer) { + return dialogContext.getActiveDialog().getState(); + } + } + + if (dialogContext.getParent() != null) { + if (dialogContext.getParent().getActiveDialog() != null) { + return dialogContext.getParent().getActiveDialog().getState(); + } + } else if (dialogContext.getActiveDialog() != null) { + return dialogContext.getActiveDialog().getState(); + } + return null; + } + + /** + * Changes the backing object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + if (memory == null) { + throw new IllegalArgumentException("memory cannot be null."); + } + + if (!(memory instanceof Map)) { + throw new IllegalArgumentException("memory must be of type Map."); + } + + // if active dialog is a container dialog then "dialog" binds to it + if (dialogContext.getActiveDialog() != null) { + Dialog dialog = dialogContext.findDialog(dialogContext.getActiveDialog().getId()); + if (dialog instanceof DialogContainer && memory instanceof Map) { + dialogContext.getActiveDialog().getState().putAll((Map) memory); + return; + } + } else if (dialogContext.getParent().getActiveDialog() != null) { + dialogContext.getParent().getActiveDialog().getState().putAll((Map) memory); + return; + } + + throw new IllegalStateException( + "Cannot set DialogMemoryScope. There is no active dialog dialog or parent dialog in the context"); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/MemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/MemoryScope.java new file mode 100644 index 000000000..f546dbc1c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/MemoryScope.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.microsoft.bot.dialogs.DialogContext; +import java.util.concurrent.CompletableFuture; + +/** + * MemoryScope represents a named memory scope abstract class. + */ +public abstract class MemoryScope { + /** + * Initializes a new instance of the class. + * + * @param name Name of the scope. + * @param includeInSnapshot Value indicating whether this memory should be included in snapshot. + */ + public MemoryScope(String name, Boolean includeInSnapshot) { + this.includeInSnapshot = includeInSnapshot; + this.name = name; + } + + /** + * Name of the scope. + */ + private String name; + + /** + * Value indicating whether this memory should be included in snapshot. + */ + private Boolean includeInSnapshot; + + /** + * @return String Gets the name of the scope. + */ + public String getName() { + return this.name; + } + + /** + * @param withName Sets the name of the scope. + */ + public void setName(String withName) { + this.name = withName; + } + + /** + * @return Boolean Returns the value indicating whether this memory should be included in snapshot. + */ + public Boolean getIncludeInSnapshot() { + return this.includeInSnapshot; + } + + + /** + * @param withIncludeInSnapshot Sets the value indicating whether this memory should be included in snapshot. + */ + public void setIncludeInSnapshot(Boolean withIncludeInSnapshot) { + this.includeInSnapshot = withIncludeInSnapshot; + } + + /** + * Get the backing memory for this scope. + * + * @param dialogContext The DialogContext to get from the memory store. + * @return Object The memory for this scope. + */ + public abstract Object getMemory(DialogContext dialogContext); + + /** + * Changes the backing object for the memory scope. + * + * @param dialogContext The DialogContext to set in memory store. + * @param memory The memory to set the DialogContext to. + */ + public abstract void setMemory(DialogContext dialogContext, Object memory); + + /** + * Populates the state cache for this from the storage layer. + * + * @param dialogContext The dialog context object for this turn. + * @param force True to overwrite any existing state cache or false to load state from storage only + * if the cache doesn't already exist. + * @return CompletableFuture A future that represents the work queued to execute. + */ + public CompletableFuture load(DialogContext dialogContext, Boolean force) { + return CompletableFuture.completedFuture(null); + } + + + /** + * Writes the state cache for this to the storage layer. + * + * @param dialogContext The dialog context Object for this turn. + * @param force True to save the state cache to storage. or false to save state to storage only + * if a property in the cache has changed. + * @return CompletableFuture A future that represents the work queued to execute. + */ + public CompletableFuture saveChanges(DialogContext dialogContext, Boolean force) { + return CompletableFuture.completedFuture(null); + } + + /** + * Deletes any state in storage and the cache for this. + * + * @param dialogContext The dialog context Object for this turn. + * @return CompletableFuture A future that represents the work queued to execute. + */ + public CompletableFuture delete(DialogContext dialogContext) { + return CompletableFuture.completedFuture(null); + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ReadOnlyObject.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ReadOnlyObject.java new file mode 100644 index 000000000..1c43f012e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ReadOnlyObject.java @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.microsoft.bot.dialogs.ObjectPath; + +/** + * ReadOnlyObject is a wrapper around any Object to prevent setting of + * properties on the Object. + */ +public final class ReadOnlyObject implements Map { + + private final String notSupported = "This Object is final and cannot be modified."; + + private Object obj; + + /** + * + * @param obj Object to wrap. Any expression properties on it will be evaluated + * using the dc. + */ + public ReadOnlyObject(Object obj) { + this.obj = obj; + } + + /** + * @return The number of items. + */ + @Override + public int size() { + return ObjectPath.getProperties(obj).size(); + } + + /** + * + */ + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(Object key) { + return ObjectPath.containsProperty(obj, (String) key); + } + + /** + * + */ + @Override + public Set keySet() { + return new HashSet(ObjectPath.getProperties(obj)); + } + + /** + * + */ + @Override + public Object get(Object key) { + + if (!(key instanceof String)) { + throw new IllegalArgumentException("key is required and must be a String type."); + } + + return ObjectPath.tryGetPathValue(obj, (String) key, Object.class); + } + + /** + * + */ + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(notSupported); + } + + /** + * + */ + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(notSupported); + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(notSupported); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(notSupported); + } + + @Override + public Collection values() { + Set keys = this.keySet(); + List objectList = new ArrayList(); + for (String key : keys) { + Object foundValue = ObjectPath.tryGetPathValue(obj, key, Object.class); + if (foundValue != null) { + objectList.add(foundValue); + } + } + return objectList; + } + + @Override + public Set> entrySet() { + Set> items = new HashSet>(); + Set keys = this.keySet(); + for (String key : keys) { + Object foundValue = ObjectPath.tryGetPathValue(obj, key, Object.class); + if (foundValue != null) { + items.add(new SimpleEntry(key, foundValue)); + } + } + return items; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/SettingsMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/SettingsMemoryScope.java new file mode 100644 index 000000000..81f05b6c4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/SettingsMemoryScope.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import java.util.Properties; +import java.util.TreeMap; + +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.ScopePath; +import com.microsoft.bot.integration.Configuration; + +/** + * TurnMemoryScope represents memory scoped to the current turn. + */ +public class SettingsMemoryScope extends MemoryScope { + /** + * Initializes a new instance of the TurnMemoryScope class. + */ + public SettingsMemoryScope() { + super(ScopePath.SETTINGS, false); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + Object returnValue; + + returnValue = dialogContext.getContext().getTurnState().get(ScopePath.TURN); + if (returnValue == null) { + Configuration configuration = dialogContext.getContext().getTurnState().get(Configuration.class); + if (configuration != null) { + returnValue = loadSettings(configuration); + dialogContext.getContext().getTurnState().add(ScopePath.SETTINGS, returnValue); + } + } + return returnValue; + } + + /** + * Changes the backing Object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + throw new UnsupportedOperationException("You cannot set the memory for a final memory scope"); + } + + /** + * Loads the settings from configuration. + * + * @param configuration The configuration to load Settings from. + * @return The collection of settings. + */ + protected static TreeMap loadSettings(Configuration configuration) { + TreeMap settings = new TreeMap(String.CASE_INSENSITIVE_ORDER); + + if (configuration != null) { + Properties properties = configuration.getProperties(); + properties.forEach((k, v) -> { + settings.put((String) k, v); + }); + } + + return settings; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ThisMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ThisMemoryScope.java new file mode 100644 index 000000000..5e43ed86f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/ThisMemoryScope.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * MemoryScope represents a named memory scope abstract class. + */ +public class ThisMemoryScope extends MemoryScope { + /** + * DialogMemoryScope maps "this" to dc.ActiveDialog.State. + */ + public ThisMemoryScope() { + super(ScopePath.THIS, true); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + if (dialogContext.getActiveDialog() != null) { + return dialogContext.getActiveDialog().getState(); + } else { + return null; + } + + } + + /** + * Changes the backing Object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + throw new UnsupportedOperationException("You can't modify the class scope."); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/TurnMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/TurnMemoryScope.java new file mode 100644 index 000000000..74c5b1553 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/TurnMemoryScope.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import java.util.TreeMap; + +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * TurnMemoryScope represents memory scoped to the current turn. + */ +public class TurnMemoryScope extends MemoryScope { + /** + * Initializes a new instance of the TurnMemoryScope class. + */ + public TurnMemoryScope() { + super(ScopePath.TURN, false); + } + + /** + * Get the backing memory for this scope. + */ + @Override + public final Object getMemory(DialogContext dialogContext) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + Object returnValue; + + returnValue = dialogContext.getContext().getTurnState().get(ScopePath.TURN); + if (returnValue == null) { + returnValue = new TreeMap(String.CASE_INSENSITIVE_ORDER); + dialogContext.getContext().getTurnState().add(ScopePath.TURN, returnValue); + } + + return returnValue; + } + + /** + * Changes the backing object for the memory scope. + */ + @Override + public final void setMemory(DialogContext dialogContext, Object memory) { + if (dialogContext == null) { + throw new IllegalArgumentException("dialogContext cannot be null."); + } + + if (dialogContext.getContext().getTurnState().containsKey(ScopePath.TURN)) { + dialogContext.getContext().getTurnState().replace(ScopePath.TURN, memory); + } else { + dialogContext.getContext().getTurnState().add(ScopePath.TURN, memory); + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/UserMemoryScope.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/UserMemoryScope.java new file mode 100644 index 000000000..09bb1d7c5 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/UserMemoryScope.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.memory.scopes; + +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.dialogs.ScopePath; + +/** + * MemoryScope represents a named memory scope abstract class. + */ +public class UserMemoryScope extends BotStateMemoryScope { + /** + * DialogMemoryScope maps "this" to dc.ActiveDialog.State. + */ + public UserMemoryScope() { + super(UserState.class, ScopePath.USER); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/package-info.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/package-info.java new file mode 100644 index 000000000..4f58e88ce --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/memory/scopes/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.dialogs.memory.scopes. + */ +package com.microsoft.bot.dialogs.memory.scopes; diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/package-info.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/package-info.java new file mode 100644 index 000000000..6664f590c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/package-info.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.dialogs. + */ +@Deprecated +package com.microsoft.bot.dialogs; diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ActivityPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ActivityPrompt.java new file mode 100644 index 000000000..9f24fd7df --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ActivityPrompt.java @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.schema.Activity; +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogInstance; +import com.microsoft.bot.dialogs.DialogReason; +import com.microsoft.bot.dialogs.DialogTurnResult; + +/** + * Defines the core behavior of a prompt dialog that waits for an activity to be + * received. + * + * This prompt requires a validator be passed in and is useful when waiting for + * non-message activities like an event to be received.The validator can ignore + * received activities until the expected activity type is received. + */ +public class ActivityPrompt extends Dialog { + + private final String persistedOptions = "options"; + private final String persistedState = "state"; + + private final PromptValidator validator; + + /** + * Initializes a new instance of the {@link ActivityPrompt} class. Called from + * constructors in derived classes to initialize the {@link ActivityPrompt} + * class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator A {@link PromptValidator} that contains validation + * for this prompt. + * + * The value of dialogId must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which the + * prompt is added. + */ + public ActivityPrompt(String dialogId, PromptValidator validator) { + super(dialogId); + if (StringUtils.isEmpty(dialogId)) { + throw new IllegalArgumentException("dialogId cannot be empty"); + } + + if (validator == null) { + throw new IllegalArgumentException("validator cannot be null"); + } + + this.validator = validator; + } + + /** + * Called when a prompt dialog is pushed onto the dialog stack and is being + * activated. + * + * @param dc The dialog context for the current turn of the conversation. + * @param options Optional, additional information to pass to the prompt being + * started. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the prompt is + * still active after the turn has been processed by the prompt. + */ + + @Override + public CompletableFuture beginDialog(DialogContext dc, Object options) { + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException("dc cannot be null.")); + } + + if (!(options instanceof PromptOptions)) { + return Async.completeExceptionally( + new IllegalArgumentException("Prompt options are required for Prompt dialogs")); + } + + // Ensure prompts have input hint set + // For Java this code isn't necessary as InputHint is an enumeration, so it's + // can't be not set to something. + // PromptOptions opt = (PromptOptions) options; + // if (opt.getPrompt() != null && + // StringUtils.isBlank(opt.getPrompt().getInputHint().toString())) { + // opt.getPrompt().setInputHint(InputHints.EXPECTING_INPUT); + // } + + // if (opt.getRetryPrompt() != null && + // StringUtils.isBlank(opt.getRetryPrompt().getInputHint().toString())) { + // opt.getRetryPrompt().setInputHint(InputHints.EXPECTING_INPUT); + // } + + // Initialize prompt state + Map state = dc.getActiveDialog().getState(); + state.put(persistedOptions, options); + + Map persistedStateMap = new HashMap(); + persistedStateMap.put(Prompt.ATTEMPTCOUNTKEY, 0); + state.put(persistedState, persistedStateMap); + + // Send initial prompt + onPrompt(dc.getContext(), (Map) state.get(persistedState), + (PromptOptions) state.get(persistedOptions), false); + + return CompletableFuture.completedFuture(END_OF_TURN); + } + + /** + * Called when a prompt dialog is the active dialog and the user replied with a + * new activity. + * + * @param dc The dialog context for the current turn of conversation. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * prompt generally continues to receive the user's replies until it + * accepts the user's reply as valid input for the prompt. + */ + @Override + public CompletableFuture continueDialog(DialogContext dc) { + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException("dc cannot be null.")); + } + + // Perform base recognition + DialogInstance instance = dc.getActiveDialog(); + Map state = (Map) instance.getState().get(persistedState); + PromptOptions options = (PromptOptions) instance.getState().get(persistedOptions); + return onRecognize(dc.getContext(), state, options).thenCompose(recognized -> { + state.put(Prompt.ATTEMPTCOUNTKEY, (int) state.get(Prompt.ATTEMPTCOUNTKEY) + 1); + return validateContext(dc, state, options, recognized).thenCompose(isValid -> { + // Return recognized value or re-prompt + if (isValid) { + return dc.endDialog(recognized.getValue()); + } + + return onPrompt(dc.getContext(), state, options, true) + .thenCompose(result -> CompletableFuture.completedFuture(END_OF_TURN)); + }); + }); + } + + private CompletableFuture validateContext(DialogContext dc, Map state, + PromptOptions options, PromptRecognizerResult recognized) { + // Validate the return value + boolean isValid = false; + if (validator != null) { + PromptValidatorContext promptContext = new PromptValidatorContext(dc.getContext(), + recognized, state, options); + return validator.promptValidator(promptContext); + } else if (recognized.getSucceeded()) { + isValid = true; + } + return CompletableFuture.completedFuture(isValid); + } + + /** + * Called when a prompt dialog resumes being the active dialog on the dialog + * stack, such as when the previous active dialog on the stack completes. + * + * @param dc The dialog context for the current turn of the conversation. + * @param reason An enum indicating why the dialog resumed. + * @param result Optional, value returned from the previous dialog on the stack. + * The type of the value returned is dependent on the previous + * dialog. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + @Override + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + // Prompts are typically leaf nodes on the stack but the dev is free to push + // other dialogs + // on top of the stack which will result in the prompt receiving an unexpected + // call to + // dialogResume() when the pushed on dialog ends. + // To avoid the prompt prematurely ending we need to implement this method and + // simply re-prompt the user. + repromptDialog(dc.getContext(), dc.getActiveDialog()); + return CompletableFuture.completedFuture(END_OF_TURN); + } + + /** + * Called when a prompt dialog has been requested to re-prompt the user for + * input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param instance The instance of the dialog on the stack. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { + Map state = (Map) instance.getState().get(persistedState); + PromptOptions options = (PromptOptions) instance.getState().get(persistedOptions); + onPrompt(turnContext, state, options, false); + return CompletableFuture.completedFuture(null); + } + + /** + * When overridden in a derived class, prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options) { + return onPrompt(turnContext, state, options, false).thenApply(result -> null); + } + + /** + * When overridden in a derived class, prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry A {@link Boolean} representing if the prompt is a retry. + * + * @return A {@link CompletableFuture} representing the result of the + * asynchronous operation. + */ + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException("turnContext cannot be null")); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException("options cannot be null")); + } + + if (isRetry && options.getRetryPrompt() != null) { + return turnContext.sendActivity(options.getRetryPrompt()).thenApply(result -> null); + } else if (options.getPrompt() != null) { + return turnContext.sendActivity(options.getPrompt()).thenApply(result -> null); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * When overridden in a derived class, attempts to recognize the incoming + * activity. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the + * recognition attempt. + */ + protected CompletableFuture> onRecognize(TurnContext turnContext, + Map state, PromptOptions options) { + PromptRecognizerResult result = new PromptRecognizerResult(); + result.setSucceeded(true); + result.setValue(turnContext.getActivity()); + + return CompletableFuture.completedFuture(result); + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java new file mode 100644 index 000000000..74dbbc514 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/AttachmentPrompt.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Attachment; + +/** + * Prompts a user to upload attachments, like images. + */ +public class AttachmentPrompt extends Prompt> { + + /** + * Initializes a new instance of the {@link AttachmentPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * + * The value of dialogId must be unique within the {@link DialogSet} or + * {@link ComponentDialog} to which the prompt is added. + */ + public AttachmentPrompt(String dialogId) { + this(dialogId, null); + } + + /** + * Initializes a new instance of the {@link AttachmentPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator} that contains additional, + * custom validation for this prompt. + * + * The value of dialogId must be unique within the {@link DialogSet} or + * {@link ComponentDialog} to which the prompt is added. + */ + public AttachmentPrompt(String dialogId, PromptValidator> validator) { + super(dialogId, validator); + } + + /** + * Prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance on the + * stack is prompting the user for input; otherwise, false. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "options cannot be null" + )); + } + + if (isRetry && options.getRetryPrompt() != null) { + return turnContext.sendActivity(options.getRetryPrompt()).thenApply(result -> null); + } else if (options.getPrompt() != null) { + return turnContext.sendActivity(options.getPrompt()).thenApply(result -> null); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Attempts to recognize the user's input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the recognition attempt. + */ + @Override + protected CompletableFuture>> onRecognize(TurnContext turnContext, + Map state, PromptOptions options) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + PromptRecognizerResult> result = new PromptRecognizerResult>(); + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + Activity message = turnContext.getActivity(); + if (message.getAttachments() != null && message.getAttachments().size() > 0) { + result.setSucceeded(true); + result.setValue(message.getAttachments()); + } + } + + return CompletableFuture.completedFuture(result); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java new file mode 100644 index 000000000..9cf48cbb7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ChoicePrompt.java @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.choices.Choice; +import com.microsoft.bot.dialogs.choices.ChoiceFactoryOptions; +import com.microsoft.bot.dialogs.choices.ChoiceRecognizers; +import com.microsoft.bot.dialogs.choices.FindChoicesOptions; +import com.microsoft.bot.dialogs.choices.FoundChoice; +import com.microsoft.bot.dialogs.choices.ListStyle; +import com.microsoft.bot.dialogs.choices.ModelResult; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; + +import org.apache.commons.lang3.StringUtils; + +/** + * Prompts a user to select from a list of choices. + */ +public class ChoicePrompt extends Prompt { + + /** + * A dictionary of Default Choices based on {@link GetSupportedCultures} . Can + * be replaced by user using the constructor that contains choiceDefaults. + */ + private Map choiceDefaults; + + private ListStyle style; + private String defaultLocale; + private FindChoicesOptions recognizerOptions; + private ChoiceFactoryOptions choiceOptions; + + /** + * Initializes a new instance of the {@link ChoicePrompt} class. + * + * @param dialogId The ID to assign to this prompt. + */ + public ChoicePrompt(String dialogId) { + this(dialogId, null, null); + } + + /** + * Initializes a new instance of the {@link ChoicePrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator{FoundChoice}} that + * contains additional, custom validation for this prompt. + * @param defaultLocale Optional, the default locale used to determine + * language-specific behavior of the prompt. The locale is + * a 2, 3, or 4 character ISO 639 code that represents a + * language or language family. + * + * The value of {@link dialogId} must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which + * the prompt is added. If the {@link Activity#locale} of + * the {@link DialogContext} .{@link DialogContext#context} + * .{@link ITurnContext#activity} is specified, then that + * local is used to determine language specific behavior; + * otherwise the {@link defaultLocale} is used. US-English + * is the used if no language or default locale is + * available, or if the language or locale is not otherwise + * supported. + */ + public ChoicePrompt(String dialogId, PromptValidator validator, String defaultLocale) { + super(dialogId, validator); + + choiceDefaults = new HashMap(); + for (PromptCultureModel model : PromptCultureModels.getSupportedCultures()) { + ChoiceFactoryOptions options = new ChoiceFactoryOptions(); + options.setInlineSeparator(model.getSeparator()); + options.setInlineOr(model.getInlineOr()); + options.setInlineOrMore(model.getInlineOrMore()); + options.setIncludeNumbers(true); + choiceDefaults.put(model.getLocale(), options); + } + + this.style = ListStyle.AUTO; + this.defaultLocale = defaultLocale; + } + + /** + * Initializes a new instance of the {@link ChoicePrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator{FoundChoice}} that contains + * additional, custom validation for this prompt. + * @param defaultLocale Optional, the default locale used to determine + * language-specific behavior of the prompt. The locale is a 2, 3, or 4 character + * ISO 639 code + * that represents a language or language family. + * @param choiceDefaults Overrides the dictionary of Bot Framework SDK-supported + * _choiceDefaults (for prompt localization). Must be passed in to each ConfirmPrompt that + * needs the custom choice defaults. + * + * The value of {@link dialogId} must be unique within the {@link DialogSet} or + * {@link ComponentDialog} to which the prompt is added. If the {@link Activity#locale} of the + * {@link DialogContext} .{@link DialogContext#context} .{@link ITurnContext#activity} is + * specified, then that local is used to determine language specific behavior; otherwise the + * {@link defaultLocale} is used. US-English is the used if no language or default locale is + * available, or if the language or locale is not otherwise supported. + */ + public ChoicePrompt(String dialogId, Map choiceDefaults, + PromptValidator validator, String defaultLocale) { + this(dialogId, validator, defaultLocale); + + this.choiceDefaults = choiceDefaults; + } + + /** + * Gets the style to use when presenting the prompt to the user. + * + * @return The style to use when presenting the prompt to the user. + */ + public ListStyle getStyle() { + return this.style; + } + + /** + * Sets the style to use when presenting the prompt to the user. + * + * @param style The style to use when presenting the prompt to the user. + */ + public void setStyle(ListStyle style) { + this.style = style; + } + + /** + * Sets or sets the default locale used to determine language-specific behavior + * of the prompt. + * + * @return The default locale used to determine language-specific behavior of + * the prompt. + */ + public String getDefaultLocale() { + return this.defaultLocale; + } + + /** + * Sets the default locale used to determine language-specific behavior of the + * prompt. + * + * @param defaultLocale The default locale used to determine language-specific + * behavior of the prompt. + */ + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Gets or sets additional options passed to the underlying + * {@link ChoiceRecognizers#recognizeChoices(String, IList{Choice}, FindChoicesOptions)} method. + * + * @return Options to control the recognition strategy. + */ + public FindChoicesOptions getRecognizerOptions() { + return this.recognizerOptions; + } + + /** + * Gets or sets additional options passed to the underlying + * {@link ChoiceRecognizers#recognizeChoices(String, IList{Choice}, FindChoicesOptions)} method. + * + * @param recognizerOptions Options to control the recognition strategy. + */ + public void setRecognizerOptions(FindChoicesOptions recognizerOptions) { + this.recognizerOptions = recognizerOptions; + } + + /** + * Gets additional options passed to the {@link ChoiceFactory} and used to tweak the + * style of choices rendered to the user. + * @return Additional options for presenting the set of choices. + */ + public ChoiceFactoryOptions getChoiceOptions() { + return this.choiceOptions; + } + + /** + * Sets additional options passed to the {@link ChoiceFactory} and used to tweak the + * style of choices rendered to the user. + * @param choiceOptions Additional options for presenting the set of choices. + */ + public void setChoiceOptions(ChoiceFactoryOptions choiceOptions) { + this.choiceOptions = choiceOptions; + } + + /** + * Prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance on the + * stack is prompting the user for input; otherwise, false. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "options cannot be null" + )); + } + + String culture = determineCulture(turnContext.getActivity()); + + // Format prompt to send + Activity prompt; + + List choices = options.getChoices() != null ? options.getChoices() : new ArrayList(); + String channelId = turnContext.getActivity().getChannelId(); + ChoiceFactoryOptions choiceOpts = getChoiceOptions() != null + ? getChoiceOptions() : choiceDefaults.get(culture); + ListStyle choiceStyle = options.getStyle() != null ? options.getStyle() : style; + + if (isRetry && options.getRetryPrompt() != null) { + prompt = appendChoices(options.getRetryPrompt(), channelId, choices, choiceStyle, choiceOpts); + } else { + prompt = appendChoices(options.getPrompt(), channelId, choices, choiceStyle, choiceOpts); + } + + // Send prompt + return turnContext.sendActivity(prompt).thenApply(result -> null); + } + + /** + * Attempts to recognize the user's input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the recognition attempt. + */ + @Override + protected CompletableFuture> onRecognize(TurnContext turnContext, + Map state, PromptOptions options) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + List choices = options.getChoices() != null ? options.getChoices() : new ArrayList(); + + PromptRecognizerResult result = new PromptRecognizerResult(); + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + Activity activity = turnContext.getActivity(); + String utterance = activity.getText(); + if (StringUtils.isEmpty(utterance)) { + return CompletableFuture.completedFuture(result); + } + + FindChoicesOptions opt = recognizerOptions != null ? recognizerOptions : new FindChoicesOptions(); + opt.setLocale(determineCulture(activity, opt)); + List> results = ChoiceRecognizers.recognizeChoices(utterance, choices, opt); + if (results != null && results.size() > 0) { + result.setSucceeded(true); + result.setValue(results.get(0).getResolution()); + } + } + return CompletableFuture.completedFuture(result); + } + + private String determineCulture(Activity activity) { + return determineCulture(activity, null); + } + + private String determineCulture(Activity activity, FindChoicesOptions opt) { + + String locale; + if (activity.getLocale() != null) { + locale = activity.getLocale(); + } else if (opt != null) { + locale = opt.getLocale(); + } else if (defaultLocale != null) { + locale = defaultLocale; + } else { + locale = PromptCultureModels.ENGLISH_CULTURE; + } + + String culture = PromptCultureModels.mapToNearestLanguage(locale); + if (StringUtils.isBlank(culture) || !choiceDefaults.containsKey(culture)) { + culture = PromptCultureModels.ENGLISH_CULTURE; + } + return culture; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java new file mode 100644 index 000000000..6352fe8e9 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/ConfirmPrompt.java @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.choices.Choice; +import com.microsoft.bot.dialogs.choices.ChoiceFactoryOptions; +import com.microsoft.bot.dialogs.choices.ChoiceRecognizers; +import com.microsoft.bot.dialogs.choices.FoundChoice; +import com.microsoft.bot.dialogs.choices.ListStyle; +import com.microsoft.bot.dialogs.choices.ModelResult; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.recognizers.text.choice.ChoiceRecognizer; + +import org.apache.commons.lang3.StringUtils; +import org.javatuples.Pair; +import org.javatuples.Triplet; + +/** + * Prompts a user to confirm something with a yes/no response. + */ +public class ConfirmPrompt extends Prompt { + + /** + * A map of Default Choices based on {@link GetSupportedCultures} . Can + * be replaced by user using the constructor that contains choiceDefaults. + */ + private Map> choiceDefaults; + private ListStyle style; + private String defaultLocale; + private ChoiceFactoryOptions choiceOptions; + private Pair confirmChoices; + + /** + * Initializes a new instance of the {@link ConfirmPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + */ + public ConfirmPrompt(String dialogId) { + this(dialogId, null, null); + } + + /** + * Initializes a new instance of the {@link ConfirmPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator{FoundChoice}} that + * contains additional, custom validation for this prompt. + * @param defaultLocale Optional, the default locale used to determine + * language-specific behavior of the prompt. The locale is + * a 2, 3, or 4 character ISO 639 code that represents a + * language or language family. + * + * The value of {@link dialogId} must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which + * the prompt is added. If the {@link Activity#locale} of + * the {@link DialogContext} .{@link DialogContext#context} + * .{@link ITurnContext#activity} is specified, then that + * local is used to determine language specific behavior; + * otherwise the {@link defaultLocale} is used. US-English + * is the used if no language or default locale is + * available, or if the language or locale is not otherwise + * supported. + */ + public ConfirmPrompt(String dialogId, PromptValidator validator, String defaultLocale) { + super(dialogId, validator); + + choiceDefaults = new HashMap>(); + for (PromptCultureModel model : PromptCultureModels.getSupportedCultures()) { + Choice yesChoice = new Choice(model.getYesInLanguage()); + Choice noChoice = new Choice(model.getNoInLanguage()); + ChoiceFactoryOptions factoryOptions = new ChoiceFactoryOptions(); + factoryOptions.setInlineSeparator(model.getSeparator()); + factoryOptions.setInlineOr(model.getInlineOr()); + factoryOptions.setInlineOrMore(model.getInlineOrMore()); + factoryOptions.setIncludeNumbers(true); + choiceDefaults.put(model.getLocale(), new Triplet(yesChoice, + noChoice, + factoryOptions)); + } + + this.style = ListStyle.AUTO; + this.defaultLocale = defaultLocale; + } + + /** + * Initializes a new instance of the {@link ConfirmPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator{FoundChoice}} that + * contains additional, custom validation for this prompt. + * @param defaultLocale Optional, the default locale used to determine + * language-specific behavior of the prompt. The locale is + * a 2, 3, or 4 character ISO 639 code that represents a + * language or language family. + * @param choiceDefaults Overrides the dictionary of Bot Framework SDK-supported + * _choiceDefaults (for prompt localization). Must be + * passed in to each ConfirmPrompt that needs the custom + * choice defaults. + * + * The value of {@link dialogId} must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which + * the prompt is added. If the {@link Activity#locale} of + * the {@link DialogContext} + * .{@link DialogContext#context} + * .{@link ITurnContext#activity} is specified, then that + * local is used to determine language specific behavior; + * otherwise the {@link defaultLocale} is used. US-English + * is the used if no language or default locale is + * available, or if the language or locale is not + * otherwise supported. + */ + public ConfirmPrompt(String dialogId, Map> choiceDefaults, + PromptValidator validator, String defaultLocale) { + this(dialogId, validator, defaultLocale); + this.choiceDefaults = choiceDefaults; + } + + + /** + * Gets the style to use when presenting the prompt to the user. + * + * @return The style to use when presenting the prompt to the user. + */ + public ListStyle getStyle() { + return this.style; + } + + /** + * Sets the style to use when presenting the prompt to the user. + * + * @param style The style to use when presenting the prompt to the user. + */ + public void setStyle(ListStyle style) { + this.style = style; + } + + /** + * Sets or sets the default locale used to determine language-specific behavior + * of the prompt. + * + * @return The default locale used to determine language-specific behavior of + * the prompt. + */ + public String getDefaultLocale() { + return this.defaultLocale; + } + + /** + * Sets the default locale used to determine language-specific behavior of the + * prompt. + * + * @param defaultLocale The default locale used to determine language-specific + * behavior of the prompt. + */ + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Gets additional options passed to the {@link ChoiceFactory} and used to tweak the + * style of choices rendered to the user. + * @return Additional options for presenting the set of choices. + */ + public ChoiceFactoryOptions getChoiceOptions() { + return this.choiceOptions; + } + + /** + * Sets additional options passed to the {@link ChoiceFactory} and used to tweak the + * style of choices rendered to the user. + * @param choiceOptions Additional options for presenting the set of choices. + */ + public void setChoiceOptions(ChoiceFactoryOptions choiceOptions) { + this.choiceOptions = choiceOptions; + } + + /** + * Gets the yes and no {@link Choice} for the prompt. + * @return The yes and no {@link Choice} for the prompt. + */ + public Pair getConfirmChoices() { + return this.confirmChoices; + } + + /** + * Sets the yes and no {@link Choice} for the prompt. + * @param confirmChoices The yes and no {@link Choice} for the prompt. + */ + public void setConfirmChoices(Pair confirmChoices) { + this.confirmChoices = confirmChoices; + } + + /** + * Prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance on the + * stack is prompting the user for input; otherwise, false. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "options cannot be null" + )); + } + + // Format prompt to send + Activity prompt; + String channelId = turnContext.getActivity().getChannelId(); + String culture = determineCulture(turnContext.getActivity()); + Triplet defaults = choiceDefaults.get(culture); + ChoiceFactoryOptions localChoiceOptions = getChoiceOptions() != null ? getChoiceOptions() + : defaults.getValue2(); + List choices = new ArrayList(Arrays.asList(defaults.getValue0(), + defaults.getValue1())); + + ListStyle localStyle = options.getStyle() != null ? options.getStyle() : getStyle(); + if (isRetry && options.getRetryPrompt() != null) { + prompt = appendChoices(options.getRetryPrompt(), channelId, + choices, localStyle, localChoiceOptions); + } else { + prompt = appendChoices(options.getPrompt(), channelId, choices, + localStyle, localChoiceOptions); + } + + // Send prompt + return turnContext.sendActivity(prompt).thenApply(result -> null); + } + + /** + * Attempts to recognize the user's input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the recognition attempt. + */ + @Override + protected CompletableFuture> onRecognize(TurnContext turnContext, + Map state, PromptOptions options) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + PromptRecognizerResult result = new PromptRecognizerResult(); + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + // Recognize utterance + String utterance = turnContext.getActivity().getText(); + if (StringUtils.isBlank(utterance)) { + return CompletableFuture.completedFuture(result); + } + + String culture = determineCulture(turnContext.getActivity()); + List results = + ChoiceRecognizer.recognizeBoolean(utterance, culture); + if (results.size() > 0) { + com.microsoft.recognizers.text.ModelResult first = results.get(0); + Boolean value = (Boolean) first.resolution.get("value"); + if (value != null) { + result.setSucceeded(true); + result.setValue(value); + } + } else { + // First check whether the prompt was sent to the user with numbers - + // if it was we should recognize numbers + Triplet defaults = choiceDefaults.get(culture); + ChoiceFactoryOptions choiceOpts = choiceOptions != null ? choiceOptions : defaults.getValue2(); + + // This logic reflects the fact that IncludeNumbers is nullable and True is the default + // set in Inline style + if (choiceOpts.getIncludeNumbers() == null + || choiceOpts.getIncludeNumbers() != null && choiceOpts.getIncludeNumbers()) { + // The text may be a number in which case we will interpret that as a choice. + Pair confirmedChoices = confirmChoices != null ? confirmChoices + : new Pair(defaults.getValue0(), defaults.getValue1()); + ArrayList choices = new ArrayList(); + choices.add(confirmedChoices.getValue0()); + choices.add(confirmedChoices.getValue1()); + + List> secondAttemptResults = + ChoiceRecognizers.recognizeChoices(utterance, choices); + if (secondAttemptResults.size() > 0) { + result.setSucceeded(true); + result.setValue(secondAttemptResults.get(0).getResolution().getIndex() == 0); + } + } + } + } + + return CompletableFuture.completedFuture(result); + } + + private String determineCulture(Activity activity) { + + String locale; + if (activity.getLocale() != null) { + locale = activity.getLocale(); + } else if (defaultLocale != null) { + locale = defaultLocale; + } else { + locale = PromptCultureModels.ENGLISH_CULTURE; + } + + String culture = PromptCultureModels.mapToNearestLanguage(locale); + if (StringUtils.isEmpty(culture) || !choiceDefaults.containsKey(culture)) { + culture = PromptCultureModels.ENGLISH_CULTURE; + } + return culture; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java new file mode 100644 index 000000000..38c491542 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimePrompt.java @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.recognizers.text.ModelResult; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.DateTimeRecognizer; + +import org.apache.commons.lang3.StringUtils; + +/** + * Prompts a user for a date-time value. + */ +public class DateTimePrompt extends Prompt> { + + private String defaultLocale; + + /** + * Initializes a new instance of the {@link DateTimePrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator.FoundChoice} that + * contains additional, custom validation for this prompt. + * @param defaultLocale Optional, the default locale used to determine + * language-specific behavior of the prompt. The locale is + * a 2, 3, or 4 character ISO 639 code that represents a + * language or language family. + * + * The value of {@link dialogId} must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which + * the prompt is added. If the {@link Activity#locale} of + * the {@link DialogContext} .{@link DialogContext#context} + * .{@link ITurnContext#activity} is specified, then that + * local is used to determine language specific behavior; + * otherwise the {@link defaultLocale} is used. US-English + * is the used if no language or default locale is + * available, or if the language or locale is not otherwise + * supported. + */ + public DateTimePrompt(String dialogId, PromptValidator> validator, String defaultLocale) { + super(dialogId, validator); + this.defaultLocale = defaultLocale; + } + + /** + * Gets the default locale used to determine language-specific behavior of the + * prompt. + * + * @return The default locale used to determine language-specific behavior of + * the prompt. + */ + public String getDefaultLocale() { + return this.defaultLocale; + } + + /** + * Sets the default locale used to determine language-specific behavior of the + * prompt. + * + * @param defaultLocale The default locale used to determine language-specific + * behavior of the prompt. + */ + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance on the + * stack is prompting the user for input; otherwise, false. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "options cannot be null" + )); + } + + if (isRetry && options.getRetryPrompt() != null) { + return turnContext.sendActivity(options.getRetryPrompt()).thenApply(result -> null); + } else if (options.getPrompt() != null) { + return turnContext.sendActivity(options.getPrompt()).thenApply(result -> null); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Attempts to recognize the user's input as a date-time value. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the recognition attempt. + */ + @Override + protected CompletableFuture>> + onRecognize(TurnContext turnContext, Map state, PromptOptions options) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + PromptRecognizerResult> result = + new PromptRecognizerResult>(); + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + String utterance = turnContext.getActivity().getText(); + if (StringUtils.isEmpty(utterance)) { + return CompletableFuture.completedFuture(result); + } + + String culture = turnContext.getActivity().getLocale() != null ? turnContext.getActivity().getLocale() + : defaultLocale != null ? defaultLocale : PromptCultureModels.ENGLISH_CULTURE; + LocalDateTime refTime = turnContext.getActivity().getLocalTimestamp() != null + ? turnContext.getActivity().getLocalTimestamp().toLocalDateTime() : null; + List results = + DateTimeRecognizer.recognizeDateTime(utterance, culture, DateTimeOptions.None, true, refTime); + if (results.size() > 0) { + // Return list of resolutions from first match + result.setSucceeded(true); + result.setValue(new ArrayList()); + List> values = (List>) results.get(0).resolution.get("values"); + for (Map mapEntry : values) { + result.getValue().add(readResolution(mapEntry)); + } + } + } + + return CompletableFuture.completedFuture(result); + } + + private static DateTimeResolution readResolution(Map resolution) { + DateTimeResolution result = new DateTimeResolution(); + + if (resolution.containsKey("timex")) { + result.setTimex(resolution.get("timex")); + } + + if (resolution.containsKey("value")) { + result.setValue(resolution.get("value")); + } + + if (resolution.containsKey("start")) { + result.setStart(resolution.get("start")); + } + + if (resolution.containsKey("end")) { + result.setEnd(resolution.get("end")); + } + return result; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimeResolution.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimeResolution.java new file mode 100644 index 000000000..156904b32 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/DateTimeResolution.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +/** + * A date-time value, as recognized by the {@link DateTimePrompt} . + * + * A value can represent a date, a time, a date and time, or a range of any of + * these. The representation of the value is determined by the locale used to + * parse the input. + */ + +public class DateTimeResolution { + + private String value; + private String start; + private String end; + private String timex; + + /** + * Gets a human-readable representation of the value, for a non-range result. + * + * @return A human-readable representation of the value, for a non-range result. + */ + public String getValue() { + return this.value; + } + + /** + * Sets a human-readable representation of the value, for a non-range result. + * + * @param value A human-readable representation of the value, for a non-range + * result. + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets a human-readable representation of the start value, for a range result. + * + * @return A human-readable representation of the start value, for a range + * result. + */ + public String getStart() { + return this.start; + } + + /** + * Sets a human-readable representation of the start value, for a range result. + * + * @param start A human-readable representation of the start value, for a range + * result. + */ + public void setStart(String start) { + this.start = start; + } + + /** + * Gets a human-readable represntation of the end value, for a range result. + * + * @return A human-readable representation of the end value, for a range result. + */ + public String getEnd() { + return this.end; + } + + /** + * Sets a human-readable represntation of the end value, for a range result. + * + * @param end A human-readable representation of the end value, for a range + * result. + */ + public void setEnd(String end) { + this.end = end; + } + + /** + * Gets the value in TIMEX format. The TIMEX format that follows the ISO 8601 + * standard. + * + * @return A TIMEX representation of the value. + */ + public String getTimex() { + return this.timex; + } + + /** + * Sets the value in TIMEX format. The TIMEX format that follows the ISO 8601 + * standard. + * + * @param timex A TIMEX representation of the value. + */ + public void setTimex(String timex) { + this.timex = timex; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java new file mode 100644 index 000000000..d211d7a17 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/NumberPrompt.java @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.recognizers.text.ModelResult; +import com.microsoft.recognizers.text.number.NumberRecognizer; +import com.microsoft.recognizers.text.numberwithunit.NumberWithUnitRecognizer; + +import org.apache.commons.lang3.StringUtils; + +/** + * Prompts a user to enter a number. + * + * The number prompt currently supports these types: {@link float} , {@link int} + * , {@link long} , {@link double} , and {@link decimal} . + * @param numeric type for this prompt, which can be int, long, double, or float. + */ +public class NumberPrompt extends Prompt { + + private String defaultLocale; + private final Class classOfNumber; + + /** + * Initializes a new instance of the {@link NumberPrompt{T}} class. + * + * @param dialogId Unique ID of the dialog within its parent + * {@link DialogSet} or {@link ComponentDialog} . + * @param classOfNumber Type of used to determine within the class what type was created for. This is required + * due to type erasure in Java not allowing checking the type of during runtime. + * @throws IllegalArgumentException thrown if a type other than int, long, float, or double are used for . + */ + public NumberPrompt(String dialogId, Class classOfNumber) + throws IllegalArgumentException { + this(dialogId, null, null, classOfNumber); + } + + /** + * Initializes a new instance of the {@link NumberPrompt{T}} class. + * + * @param dialogId Unique ID of the dialog within its parent + * {@link DialogSet} or {@link ComponentDialog} . + * @param validator Validator that will be called each time the user + * responds to the prompt. + * @param classOfNumber Type of used to determine within the class what type was created for. This is required + * due to type erasure in Java not allowing checking the type of during runtime. + * @throws IllegalArgumentException thrown if a type other than int, long, float, or double are used for . + */ + public NumberPrompt(String dialogId, PromptValidator validator, Class classOfNumber) + throws IllegalArgumentException { + this(dialogId, validator, null, classOfNumber); + } + + /** + * Initializes a new instance of the {@link NumberPrompt{T}} class. + * + * @param dialogId Unique ID of the dialog within its parent + * {@link DialogSet} or {@link ComponentDialog} . + * @param validator Validator that will be called each time the user + * responds to the prompt. + * @param defaultLocale Locale to use. + * @param classOfNumber Type of used to determine within the class what type was created for. This is required + * due to type erasure in Java not allowing checking the type of during runtime. + * @throws IllegalArgumentException thrown if a type other than int, long, float, or double are used for . + */ + public NumberPrompt(String dialogId, PromptValidator validator, String defaultLocale, Class classOfNumber) + throws IllegalArgumentException { + super(dialogId, validator); + this.defaultLocale = defaultLocale; + this.classOfNumber = classOfNumber; + + if (!(classOfNumber.getSimpleName().equals("Long") || classOfNumber.getSimpleName().equals("Integer") + || classOfNumber.getSimpleName().equals("Float") || classOfNumber.getSimpleName().equals("Double"))) { + throw new IllegalArgumentException(String.format("NumberPrompt: Type argument %s is not supported", + classOfNumber.getSimpleName())); + } + } + + /** + * Gets the default locale used to determine language-specific behavior of the + * prompt. + * + * @return The default locale used to determine language-specific behavior of + * the prompt. + */ + public String getDefaultLocale() { + return this.defaultLocale; + } + + /** + * Sets the default locale used to determine language-specific behavior of the + * prompt. + * + * @param defaultLocale The default locale used to determine language-specific + * behavior of the prompt. + */ + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance + * on the stack is prompting the user for input; otherwise, + * false. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "options cannot be null" + )); + } + + if (isRetry && options.getRetryPrompt() != null) { + return turnContext.sendActivity(options.getRetryPrompt()).thenApply(result -> null); + } else if (options.getPrompt() != null) { + return turnContext.sendActivity(options.getPrompt()).thenApply(result -> null); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Attempts to recognize the user's input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the + * recognition attempt. + */ + @Override + @SuppressWarnings("PMD") + protected CompletableFuture> onRecognize(TurnContext turnContext, + Map state, PromptOptions options) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + PromptRecognizerResult result = new PromptRecognizerResult(); + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + String utterance = turnContext.getActivity().getText(); + if (StringUtils.isEmpty(utterance)) { + return CompletableFuture.completedFuture(result); + } + + String culture = turnContext.getActivity().getLocale() != null ? turnContext.getActivity().getLocale() + : defaultLocale != null ? defaultLocale : PromptCultureModels.ENGLISH_CULTURE; + List results = recognizeNumberWithUnit(utterance, culture); + if (results != null && results.size() > 0) { + // Try to parse value based on type + String text = ""; + + // Try to parse value based on type + Object valueResolution = results.get(0).resolution.get("value"); + if (valueResolution != null) { + text = (String) valueResolution; + } + + if (classOfNumber.getSimpleName().equals("Float")) { + try { + Float value = Float.parseFloat(text); + result.setSucceeded(true); + result.setValue((T) (Object) value); + + } catch (NumberFormatException numberFormatException) { + } + } else if (classOfNumber.getSimpleName().equals("Integer")) { + try { + Integer value = Integer.parseInt(text); + result.setSucceeded(true); + result.setValue((T) (Object) value); + + } catch (NumberFormatException numberFormatException) { + } + } else if (classOfNumber.getSimpleName().equals("Long")) { + try { + Long value = Long.parseLong(text); + result.setSucceeded(true); + result.setValue((T) (Object) value); + + } catch (NumberFormatException numberFormatException) { + } + } else if (classOfNumber.getSimpleName().equals("Double")) { + try { + Double value = Double.parseDouble(text); + result.setSucceeded(true); + result.setValue((T) (Object) value); + + } catch (NumberFormatException numberFormatException) { + } + } + } + } + return CompletableFuture.completedFuture(result); + } + + private static List recognizeNumberWithUnit(String utterance, String culture) { + List number = NumberRecognizer.recognizeNumber(utterance, culture); + + if (number.size() > 0) { + // Result when it matches with a number recognizer + return number; + } else { + List result; + // Analyze every option for numberWithUnit + result = NumberWithUnitRecognizer.recognizeCurrency(utterance, culture); + if (result.size() > 0) { + return result; + } + + result = NumberWithUnitRecognizer.recognizeAge(utterance, culture); + if (result.size() > 0) { + return result; + } + + result = NumberWithUnitRecognizer.recognizeTemperature(utterance, culture); + if (result.size() > 0) { + return result; + } + + result = NumberWithUnitRecognizer.recognizeDimension(utterance, culture); + if (result.size() > 0) { + return result; + } + + return null; + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java new file mode 100644 index 000000000..c2fb156bd --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java @@ -0,0 +1,764 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.net.HttpURLConnection; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotAssert; +import com.microsoft.bot.builder.ConnectorClientBuilder; +import com.microsoft.bot.builder.InvokeResponse; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnStateConstants; +import com.microsoft.bot.builder.UserTokenProvider; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.connector.authentication.SkillValidation; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.CardAction; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.bot.schema.OAuthCard; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.SignInConstants; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.SigninCard; +import com.microsoft.bot.schema.TokenExchangeInvokeRequest; +import com.microsoft.bot.schema.TokenExchangeInvokeResponse; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; + +import org.apache.commons.lang3.StringUtils; + +/** + * Creates a new prompt that asks the user to sign in using the Bot Frameworks + * Single Sign On (SSO)service. + * + * The prompt will attempt to retrieve the users current token and if the user + * isn't signed in, itwill send them an `OAuthCard` containing a button they can + * press to signin. Depending on thechannel, the user will be sent through one + * of two possible signin flows:- The automatic signin flow where once the user + * signs in and the SSO service will forward the botthe users access token using + * either an `event` or `invoke` activity.- The "magic code" flow where once the + * user signs in they will be prompted by the SSOservice to send the bot a six + * digit code confirming their identity. This code will be sent as astandard + * `message` activity. Both flows are automatically supported by the + * `OAuthPrompt` and the only thing you need to becareful of is that you don't + * block the `event` and `invoke` activities that the prompt mightbe waiting on. + * **Note**:You should avoid persisting the access token with your bots other + * state. The Bot FrameworksSSO service will securely store the token on your + * behalf. If you store it in your bots stateit could expire or be revoked in + * between turns. When calling the prompt from within a waterfall step you + * should use the token within the stepfollowing the prompt and then let the + * token go out of scope at the end of your function. + */ +public class OAuthPrompt extends Dialog { + + private static final String PERSISTED_OPTIONS = "options"; + private static final String PERSISTED_STATE = "state"; + private static final String PERSISTED_EXPIRES = "expires"; + private static final String PERSISTED_CALLER = "caller"; + + private final OAuthPromptSettings settings; + private final PromptValidator validator; + + /** + * Initializes a new instance of the {@link OAuthPrompt} class. + * + * @param dialogId The D to assign to this prompt. + * @param settings Additional OAuth settings to use with this instance of the + * prompt. + * + * The value of {@link dialogId} must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which the + * prompt is added. + */ + public OAuthPrompt(String dialogId, OAuthPromptSettings settings) { + this(dialogId, settings, null); + } + + /** + * Initializes a new instance of the {@link OAuthPrompt} class. + * + * @param dialogId The D to assign to this prompt. + * @param settings Additional OAuth settings to use with this instance of the + * prompt. + * @param validator Optional, a {@link PromptValidator{FoundChoice}} that + * contains additional, custom validation for this prompt. + * + * The value of {@link dialogId} must be unique within the + * {@link DialogSet} or {@link ComponentDialog} to which the + * prompt is added. + */ + public OAuthPrompt(String dialogId, OAuthPromptSettings settings, PromptValidator validator) { + super(dialogId); + + if (StringUtils.isEmpty(dialogId)) { + throw new IllegalArgumentException("dialogId cannot be null."); + } + + if (settings == null) { + throw new IllegalArgumentException("settings cannot be null."); + } + + this.settings = settings; + this.validator = validator; + } + + /** + * Shared implementation of the SendOAuthCard function. This is intended for + * internal use, to consolidate the implementation of the OAuthPrompt and + * OAuthInput. Application logic should use those dialog classes. + * + * @param settings OAuthSettings. + * @param turnContext TurnContext. + * @param prompt MessageActivity. + * + * @return A {@link CompletableFuture} representing the result of the hronous + * operation. + */ + public static CompletableFuture sendOAuthCard(OAuthPromptSettings settings, TurnContext turnContext, + Activity prompt) { + BotAssert.contextNotNull(turnContext); + + BotAdapter adapter = turnContext.getAdapter(); + + if (!(adapter instanceof UserTokenProvider)) { + return Async.completeExceptionally( + new UnsupportedOperationException("OAuthPrompt.Prompt(): not supported by the current adapter")); + } + + UserTokenProvider tokenAdapter = (UserTokenProvider) adapter; + + // Ensure prompt initialized + if (prompt == null) { + prompt = Activity.createMessageActivity(); + } + + if (prompt.getAttachments() == null) { + prompt.setAttachments(new ArrayList<>()); + } + + // Append appropriate card if missing + if (!channelSupportsOAuthCard(turnContext.getActivity().getChannelId())) { + if (!prompt.getAttachments().stream().anyMatch(s -> s.getContent() instanceof SigninCard)) { + SignInResource signInResource = tokenAdapter + .getSignInResource(turnContext, settings.getOAuthAppCredentials(), settings.getConnectionName(), + turnContext.getActivity().getFrom().getId(), null) + .join(); + + CardAction cardAction = new CardAction(); + cardAction.setTitle(settings.getTitle()); + cardAction.setValue(signInResource.getSignInLink()); + cardAction.setType(ActionTypes.SIGNIN); + + ArrayList cardList = new ArrayList(); + cardList.add(cardAction); + + SigninCard signInCard = new SigninCard(); + signInCard.setText(settings.getText()); + signInCard.setButtons(cardList); + + Attachment attachment = new Attachment(); + attachment.setContentType(SigninCard.CONTENTTYPE); + attachment.setContent(signInCard); + + prompt.getAttachments().add(attachment); + + } + } else if (!prompt.getAttachments().stream().anyMatch(s -> s.getContent() instanceof OAuthCard)) { + ActionTypes cardActionType = ActionTypes.SIGNIN; + SignInResource signInResource = tokenAdapter + .getSignInResource(turnContext, settings.getOAuthAppCredentials(), settings.getConnectionName(), + turnContext.getActivity().getFrom().getId(), null) + .join(); + String value = signInResource.getSignInLink(); + + // use the SignInLink when + // in speech channel or + // bot is a skill or + // an extra OAuthAppCredentials is being passed in + ClaimsIdentity botIdentity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); + if (turnContext.getActivity().isFromStreamingConnection() + || botIdentity != null && SkillValidation.isSkillClaim(botIdentity.claims()) + || settings.getOAuthAppCredentials() != null) { + if (turnContext.getActivity().getChannelId().equals(Channels.EMULATOR)) { + cardActionType = ActionTypes.OPEN_URL; + } + } else if (!channelRequiresSignInLink(turnContext.getActivity().getChannelId())) { + value = null; + } + + CardAction cardAction = new CardAction(); + cardAction.setTitle(settings.getTitle()); + cardAction.setText(settings.getText()); + cardAction.setType(cardActionType); + cardAction.setValue(value); + + ArrayList cardList = new ArrayList(); + cardList.add(cardAction); + + OAuthCard oAuthCard = new OAuthCard(); + oAuthCard.setText(settings.getText()); + oAuthCard.setButtons(cardList); + oAuthCard.setConnectionName(settings.getConnectionName()); + oAuthCard.setTokenExchangeResource(signInResource.getTokenExchangeResource()); + + Attachment attachment = new Attachment(); + attachment.setContentType(OAuthCard.CONTENTTYPE); + attachment.setContent(oAuthCard); + + prompt.getAttachments().add(attachment); + } + + // Add the login timeout specified in OAuthPromptSettings to TurnState so it can + // be referenced if polling is needed + if (!turnContext.getTurnState().containsKey(TurnStateConstants.OAUTH_LOGIN_TIMEOUT_KEY) + && settings.getTimeout() != null) { + turnContext.getTurnState().add(TurnStateConstants.OAUTH_LOGIN_TIMEOUT_KEY, + Duration.ofMillis(settings.getTimeout())); + } + + // Set input hint + if (prompt.getInputHint() == null) { + prompt.setInputHint(InputHints.ACCEPTING_INPUT); + } + + return turnContext.sendActivity(prompt).thenApply(result -> null); + } + + /** + * Shared implementation of the RecognizeToken function. This is intended for internal use, to + * consolidate the implementation of the OAuthPrompt and OAuthInput. Application logic should + * use those dialog classes. + * + * @param settings OAuthPromptSettings. + * @param dc DialogContext. + * + * @return PromptRecognizerResult. + */ + @SuppressWarnings({"checkstyle:MethodLength", "PMD.EmptyCatchBlock"}) + public static CompletableFuture> recognizeToken( + OAuthPromptSettings settings, + DialogContext dc) { + TurnContext turnContext = dc.getContext(); + PromptRecognizerResult result = new PromptRecognizerResult(); + if (isTokenResponseEvent(turnContext)) { + Object tokenResponseObject = turnContext.getActivity().getValue(); + TokenResponse token = null; + if (tokenResponseObject != null) { + token = Serialization.getAs(tokenResponseObject, TokenResponse.class); + } + result.setSucceeded(true); + result.setValue(token); + + // fixup the turnContext's state context if this was received from a skill host caller + CallerInfo callerInfo = (CallerInfo) dc.getActiveDialog().getState().get(PERSISTED_CALLER); + if (callerInfo != null) { + // set the ServiceUrl to the skill host's Url + dc.getContext().getActivity().setServiceUrl(callerInfo.getCallerServiceUrl()); + + Object adapter = turnContext.getAdapter(); + // recreate a ConnectorClient and set it in TurnState so replies use the correct one + if (!(adapter instanceof ConnectorClientBuilder)) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt: ConnectorClientProvider interface not implemented by the current adapter" + )); + } + + ConnectorClientBuilder connectorClientProvider = (ConnectorClientBuilder) adapter; + ClaimsIdentity claimsIdentity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); + ConnectorClient connectorClient = connectorClientProvider.createConnectorClient( + dc.getContext().getActivity().getServiceUrl(), + claimsIdentity, + callerInfo.getScope()).join(); + + if (turnContext.getTurnState().get(ConnectorClient.class) != null) { + turnContext.getTurnState().replace(connectorClient); + } else { + turnContext.getTurnState().add(connectorClient); + } + } + } else if (isTeamsVerificationInvoke(turnContext)) { + HashMap values = (HashMap) turnContext.getActivity().getValue(); + String magicCode = ""; + if (values != null && values instanceof HashMap) { + magicCode = (String) values.get("state"); + } + + Object adapterObject = turnContext.getAdapter(); + if (!(adapterObject instanceof UserTokenProvider)) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt.Recognize(): not supported by the current adapter" + )); + } + + UserTokenProvider adapter = (UserTokenProvider) adapterObject; + + // Getting the token follows a different flow in Teams. At the signin completion, Teams + // will send the bot an "invoke" activity that contains a "magic" code. This code MUST + // then be used to try fetching the token from Botframework service within some time + // period. We try here. If it succeeds, we return 200 with an empty body. If it fails + // with a retriable error, we return 500. Teams will re-send another invoke in this case. + // If it fails with a non-retriable error, we return 404. Teams will not (still work in + // progress) retry in that case. + try { + TokenResponse token = adapter.getUserToken( + turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + magicCode).join(); + + if (token != null) { + result.setSucceeded(true); + result.setValue(token); + + turnContext.sendActivity(new Activity(ActivityTypes.INVOKE_RESPONSE)); + } else { + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_NOT_FOUND, null); + } + } catch (Exception e) { + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_INTERNAL_ERROR, null); + } + } else if (isTokenExchangeRequestInvoke(turnContext)) { + TokenExchangeInvokeRequest tokenExchangeRequest = Serialization.getAs(turnContext.getActivity().getValue(), + TokenExchangeInvokeRequest.class); + + if (tokenExchangeRequest == null) { + TokenExchangeInvokeResponse response = new TokenExchangeInvokeResponse(); + response.setId(null); + response.setConnectionName(settings.getConnectionName()); + response.setFailureDetail("The bot received an InvokeActivity that is missing a " + + "TokenExchangeInvokeRequest value. This is required to be " + + "sent with the InvokeActivity."); + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_BAD_REQUEST, response).join(); + } else if (!tokenExchangeRequest.getConnectionName().equals(settings.getConnectionName())) { + TokenExchangeInvokeResponse response = new TokenExchangeInvokeResponse(); + response.setId(tokenExchangeRequest.getId()); + response.setConnectionName(settings.getConnectionName()); + response.setFailureDetail("The bot received an InvokeActivity with a " + + "TokenExchangeInvokeRequest containing a ConnectionName that does not match the " + + "ConnectionName expected by the bot's active OAuthPrompt. Ensure these names match " + + "when sending the InvokeActivityInvalid ConnectionName in the " + + "TokenExchangeInvokeRequest"); + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_BAD_REQUEST, response).join(); + } else if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { + TokenExchangeInvokeResponse response = new TokenExchangeInvokeResponse(); + response.setId(tokenExchangeRequest.getId()); + response.setConnectionName(settings.getConnectionName()); + response.setFailureDetail("The bot's BotAdapter does not support token exchange " + + "operations. Ensure the bot's Adapter supports the UserTokenProvider interface."); + + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_BAD_REQUEST, response).join(); + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt.Recognize(): not supported by the current adapter" + )); + } else { + TokenResponse tokenExchangeResponse = null; + try { + UserTokenProvider adapter = (UserTokenProvider) turnContext.getAdapter(); + TokenExchangeRequest tokenExchangeReq = new TokenExchangeRequest(); + tokenExchangeReq.setToken(tokenExchangeRequest.getToken()); + tokenExchangeResponse = adapter.exchangeToken( + turnContext, + settings.getConnectionName(), + turnContext.getActivity().getFrom().getId(), + tokenExchangeReq).join(); + } catch (Exception ex) { + // Ignore Exceptions + // If token exchange failed for any reason, tokenExchangeResponse above stays null, and + // hence we send back a failure invoke response to the caller. + // This ensures that the caller shows + } + + if (tokenExchangeResponse == null || StringUtils.isBlank(tokenExchangeResponse.getToken())) { + TokenExchangeInvokeResponse tokenEIR = new TokenExchangeInvokeResponse(); + tokenEIR.setId(tokenExchangeRequest.getId()); + tokenEIR.setConnectionName(tokenExchangeRequest.getConnectionName()); + tokenEIR.setFailureDetail("The bot is unable to exchange token. Proceed with regular login."); + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_PRECON_FAILED, tokenEIR).join(); + } else { + TokenExchangeInvokeResponse tokenEIR = new TokenExchangeInvokeResponse(); + tokenEIR.setId(tokenExchangeRequest.getId()); + tokenEIR.setConnectionName(settings.getConnectionName()); + sendInvokeResponse(turnContext, HttpURLConnection.HTTP_OK, tokenEIR); + + result.setSucceeded(true); + TokenResponse finalResponse = tokenExchangeResponse; + TokenResponse response = new TokenResponse(); + response.setChannelId(finalResponse.getChannelId()); + response.setConnectionName(finalResponse.getConnectionName()); + response.setToken(finalResponse.getToken()); + result.setValue(response); + } + } + } else if (turnContext.getActivity().getType().equals(ActivityTypes.MESSAGE)) { + // regex to check if code supplied is a 6 digit numerical code (hence, a magic code). + String pattern = "(\\d{6})"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(turnContext.getActivity().getText()); + + if (m.find()) { + if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt.Recognize(): not supported by the current adapter" + )); + } + UserTokenProvider adapter = (UserTokenProvider) turnContext.getAdapter(); + TokenResponse token = adapter.getUserToken(turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + m.group(0)).join(); + if (token != null) { + result.setSucceeded(true); + result.setValue(token); + } + } + } + + return CompletableFuture.completedFuture(result); + } + + /** + * Shared implementation of the SetCallerInfoInDialogState function. This is + * intended for internal use, to consolidate the implementation of the + * OAuthPrompt and OAuthInput. Application logic should use those dialog + * classes. + * + * @param state The dialog state. + * @param context TurnContext. + */ + public static void setCallerInfoInDialogState(Map state, TurnContext context) { + state.put(PERSISTED_CALLER, createCallerInfo(context)); + } + + /** + * Called when a prompt dialog is pushed onto the dialog stack and is being activated. + * + * @param dc The dialog context for the current turn of the conversation. + * @param options Optional, additional information to pass to the prompt being started. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether the prompt is still active after the + * turn has been processed by the prompt. + */ + @Override + public CompletableFuture beginDialog(DialogContext dc, Object options) { + if (dc == null) { + return Async.completeExceptionally( + new IllegalArgumentException( + "dc cannot be null." + )); + } + + if (options != null && !(options instanceof PromptOptions)) { + return Async.completeExceptionally( + new IllegalArgumentException( + "Parameter options should be an instance of to PromptOptions if provided." + )); + } + + PromptOptions opt = (PromptOptions) options; + if (opt != null) { + // Ensure prompts have input hint set + if (opt.getPrompt() != null && opt.getPrompt().getInputHint() == null) { + opt.getPrompt().setInputHint(InputHints.ACCEPTING_INPUT); + } + + if (opt.getRetryPrompt() != null && opt.getRetryPrompt() == null) { + opt.getRetryPrompt().setInputHint(InputHints.ACCEPTING_INPUT); + } + } + + // Initialize state + int timeout = settings.getTimeout() != null ? settings.getTimeout() + : (int) TurnStateConstants.OAUTH_LOGIN_TIMEOUT_VALUE.toMillis(); + Map state = dc.getActiveDialog().getState(); + state.put(PERSISTED_OPTIONS, opt); + HashMap hMap = new HashMap(); + hMap.put(Prompt.ATTEMPTCOUNTKEY, 0); + state.put(PERSISTED_STATE, hMap); + + state.put(PERSISTED_EXPIRES, OffsetDateTime.now(ZoneId.of("UTC")).plus(timeout, ChronoUnit.MILLIS)); + setCallerInfoInDialogState(state, dc.getContext()); + + // Attempt to get the users token + if (!(dc.getContext().getAdapter() instanceof UserTokenProvider)) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt.Recognize(): not supported by the current adapter" + )); + } + + UserTokenProvider adapter = (UserTokenProvider) dc.getContext().getAdapter(); + TokenResponse output = adapter.getUserToken(dc.getContext(), + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + null).join(); + if (output != null) { + // Return token + return dc.endDialog(output); + } + + // Prompt user to login + sendOAuthCard(settings, dc.getContext(), opt != null ? opt.getPrompt() : null).join(); + return CompletableFuture.completedFuture(END_OF_TURN); + } + + /** + * Called when a prompt dialog is the active dialog and the user replied with a new activity. + * + * @param dc The dialog context for the current turn of conversation. + * + * @return A {@link CompletableFuture} representing the hronous operation. + * + * If the task is successful, the result indicates whether the dialog is still active after the + * turn has been processed by the dialog. The prompt generally continues to receive the user's + * replies until it accepts the user's reply as valid input for the prompt. + */ + @Override + public CompletableFuture continueDialog(DialogContext dc) { + if (dc == null) { + return Async.completeExceptionally( + new IllegalArgumentException( + "dc cannot be null." + )); + } + + // Check for timeout + Map state = dc.getActiveDialog().getState(); + OffsetDateTime expires = (OffsetDateTime) state.get(PERSISTED_EXPIRES); + boolean isMessage = dc.getContext().getActivity().getType().equals(ActivityTypes.MESSAGE); + + // If the incoming Activity is a message, or an Activity Type normally handled by OAuthPrompt, + // check to see if this OAuthPrompt Expiration has elapsed, and end the dialog if so. + boolean isTimeoutActivityType = isMessage + || isTokenResponseEvent(dc.getContext()) + || isTeamsVerificationInvoke(dc.getContext()) + || isTokenExchangeRequestInvoke(dc.getContext()); + + + boolean hasTimedOut = isTimeoutActivityType && OffsetDateTime.now(ZoneId.of("UTC")).compareTo(expires) > 0; + + if (hasTimedOut) { + // if the token fetch request times out, complete the prompt with no result. + return dc.endDialog(); + } + + // Recognize token + PromptRecognizerResult recognized = recognizeToken(settings, dc).join(); + + Map promptState = (Map) state.get(PERSISTED_STATE); + PromptOptions promptOptions = (PromptOptions) state.get(PERSISTED_OPTIONS); + + // Increment attempt count + // Convert.ToInt32 For issue https://github.com/Microsoft/botbuilder-dotnet/issues/1859 + promptState.put(Prompt.ATTEMPTCOUNTKEY, (int) promptState.get(Prompt.ATTEMPTCOUNTKEY) + 1); + + // Validate the return value + boolean isValid = false; + if (validator != null) { + PromptValidatorContext promptContext = new PromptValidatorContext( + dc.getContext(), + recognized, + promptState, + promptOptions); + isValid = validator.promptValidator(promptContext).join(); + } else if (recognized.getSucceeded()) { + isValid = true; + } + + // Return recognized value or re-prompt + if (isValid) { + return dc.endDialog(recognized.getValue()); + } else if (isMessage && settings.getEndOnInvalidMessage()) { + // If EndOnInvalidMessage is set, complete the prompt with no result. + return dc.endDialog(); + } + + if (!dc.getContext().getResponded() + && isMessage + && promptOptions != null + && promptOptions.getRetryPrompt() != null) { + dc.getContext().sendActivity(promptOptions.getRetryPrompt()); + } + + return CompletableFuture.completedFuture(END_OF_TURN); + } + + /** + * Attempts to get the user's token. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * + * @return A task that represents the work queued to execute. + * + * If the task is successful and user already has a token or the user + * successfully signs in, the result contains the user's token. + */ + public CompletableFuture getUserToken(TurnContext turnContext) { + if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt.GetUserToken(): not supported by the current adapter" + )); + } + return ((UserTokenProvider) turnContext.getAdapter()).getUserToken(turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), null); + } + + /** + * Signs out the user. + * + * @param turnContext Context for the current turn of conversation with the user. + * + * @return A task that represents the work queued to execute. + */ + public CompletableFuture signOutUser(TurnContext turnContext) { + if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { + return Async.completeExceptionally( + new UnsupportedOperationException( + "OAuthPrompt.SignOutUser(): not supported by the current adapter" + )); + } + String id = ""; + if (turnContext.getActivity() != null + && turnContext.getActivity() != null + && turnContext.getActivity().getFrom() != null) { + id = turnContext.getActivity().getFrom().getId(); + } + + // Sign out user + return ((UserTokenProvider) turnContext.getAdapter()).signOutUser(turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + id); + } + + private static CallerInfo createCallerInfo(TurnContext turnContext) { + ClaimsIdentity botIdentity = + turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY) instanceof ClaimsIdentity + ? (ClaimsIdentity) turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY) + : null; + + if (botIdentity != null && SkillValidation.isSkillClaim(botIdentity.claims())) { + CallerInfo callerInfo = new CallerInfo(); + callerInfo.setCallerServiceUrl(turnContext.getActivity().getServiceUrl()); + callerInfo.setScope(JwtTokenValidation.getAppIdFromClaims(botIdentity.claims())); + + return callerInfo; + } + + return null; + } + + private static boolean isTokenResponseEvent(TurnContext turnContext) { + Activity activity = turnContext.getActivity(); + return activity.getType().equals(ActivityTypes.EVENT) + && activity.getName().equals(SignInConstants.TOKEN_RESPONSE_EVENT_NAME); + } + + private static boolean isTeamsVerificationInvoke(TurnContext turnContext) { + Activity activity = turnContext.getActivity(); + return activity.getType().equals(ActivityTypes.INVOKE) + && activity.getName().equals(SignInConstants.VERIFY_STATE_OPERATION_NAME); + } + + private static boolean isTokenExchangeRequestInvoke(TurnContext turnContext) { + Activity activity = turnContext.getActivity(); + return activity.getType().equals(ActivityTypes.INVOKE) + && activity.getName().equals(SignInConstants.TOKEN_EXCHANGE_OPERATION_NAME); + } + + private static boolean channelSupportsOAuthCard(String channelId) { + switch (channelId) { + case Channels.CORTANA: + case Channels.SKYPE: + case Channels.SKYPEFORBUSINESS: + return false; + default: + return true; + } + } + + private static boolean channelRequiresSignInLink(String channelId) { + switch (channelId) { + case Channels.MSTEAMS: + return true; + default: + return false; + } + } + + private static CompletableFuture sendInvokeResponse(TurnContext turnContext, int statusCode, + Object body) { + Activity activity = new Activity(ActivityTypes.INVOKE_RESPONSE); + activity.setValue(new InvokeResponse(statusCode, body)); + return turnContext.sendActivity(activity).thenApply(result -> null); + } + + /** + * Class to contain CallerInfo data including callerServiceUrl and scope. + */ + private static class CallerInfo { + + private String callerServiceUrl; + + private String scope; + + /** + * @return the CallerServiceUrl value as a String. + */ + public String getCallerServiceUrl() { + return this.callerServiceUrl; + } + + /** + * @param withCallerServiceUrl The CallerServiceUrl value. + */ + public void setCallerServiceUrl(String withCallerServiceUrl) { + this.callerServiceUrl = withCallerServiceUrl; + } + /** + * @return the Scope value as a String. + */ + public String getScope() { + return this.scope; + } + + /** + * @param withScope The Scope value. + */ + public void setScope(String withScope) { + this.scope = withScope; + } + + } +} + diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPromptSettings.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPromptSettings.java new file mode 100644 index 000000000..91e2ff436 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPromptSettings.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.dialogs.prompts; + +import com.microsoft.bot.connector.authentication.AppCredentials; + +/** + * Contains settings for an {@link OAuthPrompt}/>. + */ +public class OAuthPromptSettings { + + private AppCredentials oAuthAppCredentials; + + private String connectionName; + + private String title; + + private String text; + + private Integer timeout; + + private boolean endOnInvalidMessage; + + /** + * Gets the OAuthAppCredentials for OAuthPrompt. + * + * @return the OAuthAppCredentials value as a AppCredentials. + */ + public AppCredentials getOAuthAppCredentials() { + return this.oAuthAppCredentials; + } + + /** + * Sets the OAuthAppCredentials for OAuthPrompt. + * + * @param withOAuthAppCredentials The OAuthAppCredentials value. + */ + public void setOAuthAppCredentials(AppCredentials withOAuthAppCredentials) { + this.oAuthAppCredentials = withOAuthAppCredentials; + } + + /** + * Gets the name of the OAuth connection. + * + * @return the ConnectionName value as a String. + */ + public String getConnectionName() { + return this.connectionName; + } + + /** + * Sets the name of the OAuth connection. + * + * @param withConnectionName The ConnectionName value. + */ + public void setConnectionName(String withConnectionName) { + this.connectionName = withConnectionName; + } + + /** + * Gets the title of the sign-in card. + * + * @return the Title value as a String. + */ + public String getTitle() { + return this.title; + } + + /** + * Sets the title of the sign-in card. + * + * @param withTitle The Title value. + */ + public void setTitle(String withTitle) { + this.title = withTitle; + } + + /** + * Gets any additional text to include in the sign-in card. + * + * @return the Text value as a String. + */ + public String getText() { + return this.text; + } + + /** + * Sets any additional text to include in the sign-in card. + * + * @param withText The Text value. + */ + public void setText(String withText) { + this.text = withText; + } + + /** + * Gets the number of milliseconds the prompt waits for the user to + * authenticate. Default is 900,000 (15 minutes). + * + * @return the Timeout value as a int?. + */ + public Integer getTimeout() { + return this.timeout; + } + + /** + * Sets the number of milliseconds the prompt waits for the user to + * authenticate. Default is 900,000 (15 minutes). + * + * @param withTimeout The Timeout value. + */ + public void setTimeout(Integer withTimeout) { + this.timeout = withTimeout; + } + + /** + * Gets a value indicating whether the {@link OAuthPrompt} should end upon + * receiving an invalid message. Generally the {@link OAuthPrompt} will ignore + * incoming messages from the user during the auth flow, if they are not related + * to the auth flow. This flag enables ending the {@link OAuthPrompt} rather + * than ignoring the user's message. Typically, this flag will be set to 'true', + * but is 'false' by default for backwards compatibility. + * + * @return the EndOnInvalidMessage value as a boolean. + */ + public boolean getEndOnInvalidMessage() { + return this.endOnInvalidMessage; + } + + /** + * Sets a value indicating whether the {@link OAuthPrompt} should end upon + * receiving an invalid message. Generally the {@link OAuthPrompt} will ignore + * incoming messages from the user during the auth flow, if they are not related + * to the auth flow. This flag enables ending the {@link OAuthPrompt} rather + * than ignoring the user's message. Typically, this flag will be set to 'true', + * but is 'false' by default for backwards compatibility. + * + * @param withEndOnInvalidMessage The EndOnInvalidMessage value. + */ + public void setEndOnInvalidMessage(boolean withEndOnInvalidMessage) { + this.endOnInvalidMessage = withEndOnInvalidMessage; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java new file mode 100644 index 000000000..b16caa43b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogEvent; +import com.microsoft.bot.dialogs.DialogEvents; +import com.microsoft.bot.dialogs.DialogInstance; +import com.microsoft.bot.dialogs.DialogReason; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.choices.Choice; +import com.microsoft.bot.dialogs.choices.ChoiceFactory; +import com.microsoft.bot.dialogs.choices.ChoiceFactoryOptions; +import com.microsoft.bot.dialogs.choices.ListStyle; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.InputHints; + +import org.apache.commons.lang3.StringUtils; + +/** + * Defines the core behavior of prompt dialogs. + * + * When the prompt ends, it should return a Object that represents the value + * that was prompted for. Use + * {@link com.microsoft.bot.dialogs.DialogSet#add(Dialog)} or + * {@link com.microsoft.bot.dialogs.ComponentDialog#addDialog(Dialog)} to add a + * prompt to a dialog set or component dialog, respectively. Use + * {@link DialogContext#prompt(String, PromptOptions)} or + * {@link DialogContext#beginDialog(String, Object)} to start the prompt. If you + * start a prompt from a {@link com.microsoft.bot.dialogs.WaterfallStep} in a + * {@link com.microsoft.bot.dialogs.WaterfallDialog}, then the prompt result + * will be available in the next step of the waterfall. + * + * @param Type the prompt is created for. + */ +public abstract class Prompt extends Dialog { + + public static final String ATTEMPTCOUNTKEY = "AttemptCount"; + + private static final String PERSISTED_OPTIONS = "options"; + private static final String PERSISTED_STATE = "state"; + private final PromptValidator validator; + + /** + * Initializes a new instance of the {@link Prompt{T}} class. Called from + * constructors in derived classes to initialize the {@link Prompt{T}} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator{T}} that contains + * additional, custom validation for this prompt. + * + * The value of dialogId must be unique within the + * {@link com.microsoft.bot.dialogs.DialogSet} or + * {@link com.microsoft.bot.dialogs.ComponentDialog} to which + * the prompt is added. + */ + public Prompt(String dialogId, PromptValidator validator) { + super(dialogId); + if (StringUtils.isBlank(dialogId)) { + throw new IllegalArgumentException("dialogId cannot be null"); + } + this.validator = validator; + } + + /** + * Called when a prompt dialog is pushed onto the dialog stack and is being + * activated. + * + * @param dc The dialog context for the current turn of the conversation. + * @param options Optional, additional information to pass to the prompt being + * started. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the prompt is + * still active after the turn has been processed by the prompt. + */ + @Override + public CompletableFuture beginDialog(DialogContext dc, Object options) { + + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException("dc cannot be null.")); + } + + if (!(options instanceof PromptOptions)) { + return Async.completeExceptionally( + new IllegalArgumentException("Prompt options are required for Prompt dialogs")); + } + + // Ensure prompts have input hint set + PromptOptions opt = (PromptOptions) options; + + if (opt.getPrompt() != null && opt.getPrompt().getInputHint() == null) { + opt.getPrompt().setInputHint(InputHints.EXPECTING_INPUT); + } + + if (opt.getRetryPrompt() != null && opt.getRetryPrompt().getInputHint() == null) { + opt.getRetryPrompt().setInputHint(InputHints.EXPECTING_INPUT); + } + + // Initialize prompt state + Map state = dc.getActiveDialog().getState(); + state.put(PERSISTED_OPTIONS, opt); + + HashMap pState = new HashMap(); + pState.put(ATTEMPTCOUNTKEY, 0); + state.put(PERSISTED_STATE, pState); + + // Send initial prompt + onPrompt(dc.getContext(), (Map) state.get(PERSISTED_STATE), + (PromptOptions) state.get(PERSISTED_OPTIONS), false); + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + } + + /** + * Called when a prompt dialog is the active dialog and the user replied with a + * new activity. + * + * @param dc The dialog context for the current turn of conversation. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. The + * prompt generally continues to receive the user's replies until it + * accepts the user's reply as valid input for the prompt. + */ + @Override + public CompletableFuture continueDialog(DialogContext dc) { + + if (dc == null) { + return Async.completeExceptionally(new IllegalArgumentException("dc cannot be null.")); + } + + // Don't do anything for non-message activities + if (!dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + } + + // Perform base recognition + DialogInstance instance = dc.getActiveDialog(); + Map state = (Map) instance.getState().get(PERSISTED_STATE); + PromptOptions options = (PromptOptions) instance.getState().get(PERSISTED_OPTIONS); + return onRecognize(dc.getContext(), state, options).thenCompose(recognized -> { + state.put(ATTEMPTCOUNTKEY, (int) state.get(ATTEMPTCOUNTKEY) + 1); + + // Validate the return value + return validateContext(dc, state, options, recognized).thenCompose(isValid -> { + // Return recognized value or re-prompt + if (isValid) { + return dc.endDialog(recognized.getValue()); + } + + if (!dc.getContext().getResponded()) { + return onPrompt(dc.getContext(), state, options, true).thenApply(result -> Dialog.END_OF_TURN); + } + + return CompletableFuture.completedFuture(Dialog.END_OF_TURN); + }); + }); + } + + private CompletableFuture validateContext(DialogContext dc, Map state, + PromptOptions options, PromptRecognizerResult recognized) { + Boolean isValid = false; + if (validator != null) { + PromptValidatorContext promptContext = new PromptValidatorContext(dc.getContext(), recognized, state, + options); + return validator.promptValidator(promptContext); + } else if (recognized.getSucceeded()) { + isValid = true; + } + return CompletableFuture.completedFuture(isValid); + } + + /** + * Called when a prompt dialog resumes being the active dialog on the dialog + * stack, such as when the previous active dialog on the stack completes. + * + * @param dc The dialog context for the current turn of the conversation. + * @param reason An enum indicating why the dialog resumed. + * @param result Optional, value returned from the previous dialog on the stack. + * The type of the value returned is dependent on the previous + * dialog. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result indicates whether the dialog is + * still active after the turn has been processed by the dialog. + */ + @Override + public CompletableFuture resumeDialog(DialogContext dc, DialogReason reason, Object result) { + + // Prompts are typically leaf nodes on the stack but the dev is free to push + // other dialogs + // on top of the stack which will result in the prompt receiving an unexpected + // call to + // dialogResume() when the pushed on dialog ends. + // To avoid the prompt prematurely ending we need to implement this method and + // simply re-prompt the user. + return repromptDialog(dc.getContext(), dc.getActiveDialog()).thenApply(finalResult -> Dialog.END_OF_TURN); + } + + /** + * Called when a prompt dialog has been requested to re-prompt the user for + * input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param instance The instance of the dialog on the stack. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { + Map state = (Map) instance.getState().get(PERSISTED_STATE); + PromptOptions options = (PromptOptions) instance.getState().get(PERSISTED_OPTIONS); + return onPrompt(turnContext, state, options, false).thenApply(result -> null); + } + + /** + * Called before an event is bubbled to its parent. + * + * This is a good place to perform interception of an event as returning `true` + * will prevent any further bubbling of the event to the dialogs parents and + * will also prevent any child dialogs from performing their default processing. + * + * @param dc The dialog context for the current turn of conversation. + * @param e The event being raised. + * + * @return Whether the event is handled by the current dialog and further + * processing should stop. + */ + @Override + protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) { + if (e.getName().equals(DialogEvents.ACTIVITY_RECEIVED) + && dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + // Perform base recognition + Map state = dc.getActiveDialog().getState(); + return onRecognize(dc.getContext(), (Map) state.get(PERSISTED_STATE), + (PromptOptions) state.get(PERSISTED_OPTIONS)) + .thenCompose(recognized -> CompletableFuture.completedFuture(recognized.getSucceeded())); + } + + return CompletableFuture.completedFuture(false); + } + + /** + * When overridden in a derived class, prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance + * is on the stack is prompting the user for input; + * otherwise, false. Determines whether + * {@link PromptOptions#getPrompt()} or + * {@link PromptOptions#getRetryPrompt()} should be used. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + protected abstract CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry); + + /** + * When overridden in a derived class, attempts to recognize the user's input. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param state Contains state for the current instance of the prompt on + * the dialog stack. + * @param options A prompt options Object constructed from the options + * initially provided in the call to + * {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the + * recognition attempt. + */ + protected abstract CompletableFuture> onRecognize(TurnContext turnContext, + Map state, PromptOptions options); + + /** + * When overridden in a derived class, appends choices to the activity when the + * user is prompted for input. + * + * @param prompt The activity to append the choices to. + * @param channelId The ID of the user's channel. + * @param choices The choices to append. + * @param style Indicates how the choices should be presented to the user. + * @param options The formatting options to use when presenting the choices. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result contains the updated activity. + */ + protected Activity appendChoices(Activity prompt, String channelId, List choices, ListStyle style, + ChoiceFactoryOptions options) { + // Get base prompt text (if any) + String text = ""; + if (prompt != null && prompt.getText() != null && StringUtils.isNotBlank(prompt.getText())) { + text = prompt.getText(); + } + + // Create temporary msg + Activity msg; + switch (style) { + case INLINE: + msg = ChoiceFactory.inline(choices, text, null, options); + break; + + case LIST: + msg = ChoiceFactory.list(choices, text, null, options); + break; + + case SUGGESTED_ACTION: + msg = ChoiceFactory.suggestedAction(choices, text); + break; + + case HEROCARD: + msg = ChoiceFactory.heroCard(choices, text); + break; + + case NONE: + msg = Activity.createMessageActivity(); + msg.setText(text); + break; + + default: + msg = ChoiceFactory.forChannel(channelId, choices, text, null, options); + break; + } + + // Update prompt with text, actions and attachments + if (prompt != null) { + // clone the prompt the set in the options (note ActivityEx has Properties so + // this is the safest mechanism) + // prompt = + // JsonConvert.DeserializeObject(JsonConvert.SerializeObject(prompt)); + prompt = Activity.clone(prompt); + + prompt.setText(msg.getText()); + + if (msg.getSuggestedActions() != null && msg.getSuggestedActions().getActions() != null + && msg.getSuggestedActions().getActions().size() > 0) { + prompt.setSuggestedActions(msg.getSuggestedActions()); + } + + if (msg.getAttachments() != null && msg.getAttachments().size() > 0) { + if (prompt.getAttachments() == null) { + prompt.setAttachments(msg.getAttachments()); + } else { + List allAttachments = prompt.getAttachments(); + prompt.getAttachments().addAll(msg.getAttachments()); + prompt.setAttachments(allAttachments); + } + } + + return prompt; + } + + msg.setInputHint(InputHints.EXPECTING_INPUT); + return msg; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptCultureModel.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptCultureModel.java new file mode 100644 index 000000000..cbd369b8f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptCultureModel.java @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +/** + * Culture model used in Choice and Confirm Prompts. + */ +public class PromptCultureModel { + + private String locale; + private String separator; + private String inlineOr; + private String inlineOrMore; + private String yesInLanguage; + private String noInLanguage; + + /** + * Creates a PromptCultureModel. + */ + public PromptCultureModel() { + + } + + /** + * Gets Culture Model's Locale. + * + * @return Ex: Locale. Example: "en-US". + */ + public String getLocale() { + return this.locale; + } + + /** + * Sets Culture Model's Locale. + * + * @param locale Ex: Locale. Example: "en-US". + */ + public void setLocale(String locale) { + this.locale = locale; + } + + /** + * GetsCulture Model's Inline Separator. + * + * @return Example: ", ". + */ + public String getSeparator() { + return this.separator; + } + + /** + * Sets Culture Model's Inline Separator. + * + * @param separator Example: ", ". + */ + public void setSeparator(String separator) { + this.separator = separator; + } + + /** + * Gets Culture Model's InlineOr. + * + * @return Example: " or ". + */ + public String getInlineOr() { + return this.inlineOr; + } + + /** + * Sets Culture Model's InlineOr. + * + * @param inlineOr Example: " or ". + */ + public void setInlineOr(String inlineOr) { + this.inlineOr = inlineOr; + } + + /** + * Gets Culture Model's InlineOrMore. + * + * @return Example: ", or ". + */ + public String getInlineOrMore() { + return this.inlineOrMore; + } + + /** + * Sets Culture Model's InlineOrMore. + * + * @param inlineOrMore Example: ", or ". + */ + public void setInlineOrMore(String inlineOrMore) { + this.inlineOrMore = inlineOrMore; + } + + /** + * Gets Equivalent of "Yes" in Culture Model's Language. + * + * @return Example: "Yes". + */ + public String getYesInLanguage() { + return this.yesInLanguage; + } + + /** + * Sets Equivalent of "Yes" in Culture Model's Language. + * + * @param yesInLanguage Example: "Yes". + */ + public void setYesInLanguage(String yesInLanguage) { + this.yesInLanguage = yesInLanguage; + } + + /** + * Gets Equivalent of "No" in Culture Model's Language. + * + * @return Example: "No". + */ + public String getNoInLanguage() { + return this.noInLanguage; + } + + /** + * Sets Equivalent of "No" in Culture Model's Language. + * + * @param noInLanguage Example: "No". + */ + public void setNoInLanguage(String noInLanguage) { + this.noInLanguage = noInLanguage; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptCultureModels.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptCultureModels.java new file mode 100644 index 000000000..edb733220 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptCultureModels.java @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Class container for currently-supported Culture Models in Confirm and Choice + * Prompt. + */ +public final class PromptCultureModels { + + private PromptCultureModels() { + + } + + public static final String BULGARIAN_CULTURE = "bg-bg"; + public static final String CHINESE_CULTURE = "zh-cn"; + public static final String DUTCH_CULTURE = "nl-nl"; + public static final String ENGLISH_CULTURE = "en-us"; + public static final String FRENCH_CULTURE = "fr-fr"; + public static final String GERMAN_CULTURE = "de-de"; + public static final String HINDI_CULTURE = "hi-in"; + public static final String ITALIAN_CULTURE = "it-it"; + public static final String JAPANESE_CULTURE = "ja-jp"; + public static final String KOREAN_CULTURE = "ko-kr"; + public static final String PORTUGUESE_CULTURE = "pt-br"; + public static final String SPANISH_CULTURE = "es-es"; + public static final String SWEDISH_CULTURE = "sv-se"; + public static final String TURKISH_CULTURE = "tr-tr"; + + /** + * Gets the bulgarian prompt culture model. + */ + public static final PromptCultureModel BULGARIAN = new PromptCultureModel() { + { + setInlineOr(" или "); + setInlineOrMore(", или "); + setLocale(BULGARIAN_CULTURE); + setNoInLanguage("Не"); + setSeparator(", "); + setYesInLanguage("да"); + } + }; + + public static final PromptCultureModel CHINESE = new PromptCultureModel() { + { + setInlineOr(" 要么 "); + setInlineOrMore(", 要么 "); + setLocale(CHINESE_CULTURE); + setNoInLanguage("不"); + setSeparator(", "); + setYesInLanguage("是的"); + } + }; + + /** + * Gets the dutch prompt culture model. + */ + public static final PromptCultureModel DUTCH = new PromptCultureModel() { + { + setInlineOr(" of "); + setInlineOrMore(", of "); + setLocale(DUTCH_CULTURE); + setNoInLanguage("Nee"); + setSeparator(", "); + setYesInLanguage("Ja"); + } + }; + + /** + * Gets the english prompt culture model. + */ + public static final PromptCultureModel ENGLISH = new PromptCultureModel() { + { + setInlineOr(" or "); + setInlineOrMore(", or "); + setLocale(ENGLISH_CULTURE); + setNoInLanguage("No"); + setSeparator(", "); + setYesInLanguage("Yes"); + } + }; + + /** + * Gets the french prompt culture model. + */ + public static final PromptCultureModel FRENCH = new PromptCultureModel() { + { + setInlineOr(" ou "); + setInlineOrMore(", ou "); + setLocale(FRENCH_CULTURE); + setNoInLanguage("Non"); + setSeparator(", "); + setYesInLanguage("Oui"); + } + }; + + /** + * Gets the german prompt culture model. + */ + public static final PromptCultureModel GERMAN = new PromptCultureModel() { + { + setInlineOr(" oder "); + setInlineOrMore(", oder "); + setLocale(GERMAN_CULTURE); + setNoInLanguage("Nein"); + setSeparator(", "); + setYesInLanguage("Ja"); + } + }; + + /** + * Gets the hindi prompt culture model. + */ + public static final PromptCultureModel HINDI = new PromptCultureModel() { + { + setInlineOr(" या "); + setInlineOrMore(", या "); + setLocale(HINDI_CULTURE); + setNoInLanguage("नहीं"); + setSeparator(", "); + setYesInLanguage("हां"); + } + }; + + /** + * Gets the italian prompt culture model. + */ + public static final PromptCultureModel ITALIAN = new PromptCultureModel() { + { + setInlineOr(" o "); + setInlineOrMore(" o "); + setLocale(ITALIAN_CULTURE); + setNoInLanguage("No"); + setSeparator(", "); + setYesInLanguage("Si"); + } + }; + + /** + * Gets the japanese prompt culture model. + */ + public static final PromptCultureModel JAPANESE = new PromptCultureModel() { + { + setInlineOr(" または "); + setInlineOrMore("、 または "); + setLocale(JAPANESE_CULTURE); + setNoInLanguage("いいえ"); + setSeparator("、 "); + setYesInLanguage("はい"); + } + }; + + /** + * Gets the korean prompt culture model. + */ + public static final PromptCultureModel KOREAN = new PromptCultureModel() { + { + setInlineOr(" 또는 "); + setInlineOrMore(" 또는 "); + setLocale(KOREAN_CULTURE); + setNoInLanguage("아니"); + setSeparator(", "); + setYesInLanguage("예"); + } + }; + + /** + * Gets the portuguese prompt culture model. + */ + public static final PromptCultureModel PORTUGUESE = new PromptCultureModel() { + { + setInlineOr(" ou "); + setInlineOrMore(", ou "); + setLocale(PORTUGUESE_CULTURE); + setNoInLanguage("Não"); + setSeparator(", "); + setYesInLanguage("Sim"); + } + }; + + /** + * Gets the spanish prompt culture model. + */ + public static final PromptCultureModel SPANISH = new PromptCultureModel() { + { + setInlineOr(" o "); + setInlineOrMore(", o "); + setLocale(SPANISH_CULTURE); + setNoInLanguage("No"); + setSeparator(", "); + setYesInLanguage("Sí"); + } + }; + + /** + * Gets the swedish prompt culture model. + */ + public static final PromptCultureModel SWEDISH = new PromptCultureModel() { + { + setInlineOr(" eller "); + setInlineOrMore(" eller "); + setLocale(SWEDISH_CULTURE); + setNoInLanguage("Nej"); + setSeparator(", "); + setYesInLanguage("Ja"); + } + }; + + /** + * Gets the turkish prompt culture model. + */ + public static final PromptCultureModel TURKISH = new PromptCultureModel() { + { + setInlineOr(" veya "); + setInlineOrMore(" veya "); + setLocale(TURKISH_CULTURE); + setNoInLanguage("Hayır"); + setSeparator(", "); + setYesInLanguage("Evet"); + } + }; + + private static PromptCultureModel[] promptCultureModelArray = + { + BULGARIAN, + CHINESE, + DUTCH, + ENGLISH, + FRENCH, + GERMAN, + HINDI, + ITALIAN, + JAPANESE, + KOREAN, + PORTUGUESE, + SPANISH, + SWEDISH, + TURKISH + }; + + /** + * Gets a list of the supported culture models. + * + * @return Array of {@link PromptCultureModel} with the supported cultures. + */ + public static PromptCultureModel[] getSupportedCultures() { + return promptCultureModelArray; + } + + private static final List SUPPORTED_LOCALES = Arrays.stream(getSupportedCultures()) + .map(x -> x.getLocale()).collect(Collectors.toList()); + + // private static List supportedlocales; + + // static { + // supportedlocales = new ArrayList(); + // PromptCultureModel[] cultures = getSupportedCultures(); + // for (PromptCultureModel promptCultureModel : cultures) { + // supportedlocales.add(promptCultureModel.getLocale()); + // } + // } + + /** + * Use Recognizers-Text to normalize various potential setLocale strings to a standard. + * + * This is mostly a copy/paste from + * https://github.com/microsoft/Recognizers-Text/blob/master/.NET/Microsoft.Recognizers.Text/C + * lture.cs#L66 This doesn't directly use Recognizers-Text's MapToNearestLanguage because if + * they add language support before we do, it will break our prompts. + * + * @param cultureCode Represents setLocale. Examples: "en-US, en-us, EN". + * + * @return Normalized setLocale. + */ + public static String mapToNearestLanguage(String cultureCode) { + cultureCode = cultureCode.toLowerCase(); + final String cCode = cultureCode; + + if (SUPPORTED_LOCALES.stream().allMatch(o -> o != cCode)) { + // Handle cases like EnglishOthers with cultureCode "en-*" + List fallbackCultureCodes = SUPPORTED_LOCALES.stream() + .filter(o -> o.endsWith("*") + && cCode.startsWith(o.split("-")[0])) + .collect(Collectors.toList()); + + if (fallbackCultureCodes.size() == 1) { + return fallbackCultureCodes.get(0); + } + + //If there is no cultureCode like "-*", map only the prefix + //For example, "es-mx" will be mapped to "es-es" + fallbackCultureCodes = SUPPORTED_LOCALES.stream() + .filter(o -> cCode.startsWith(o.split("-")[0])) + .collect(Collectors.toList()); + + if (fallbackCultureCodes.size() > 0) { + return fallbackCultureCodes.get(0); + } + } + + return cultureCode; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptOptions.java new file mode 100644 index 000000000..33ec403e1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptOptions.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import com.microsoft.bot.dialogs.choices.Choice; +import com.microsoft.bot.dialogs.choices.ListStyle; +import com.microsoft.bot.schema.Activity; +import java.util.List; + + +/** + * Contains settings to pass to a {@link com.} when the prompt is started. + */ +public class PromptOptions { + + private Activity prompt; + + private Activity retryPrompt; + + private List choices; + + private ListStyle style; + + private Object validations; + + + /** + * @return Activity + */ + public Activity getPrompt() { + return this.prompt; + } + + + /** + * @param withPrompt value to set the Prompt property to + */ + public void setPrompt(Activity withPrompt) { + this.prompt = withPrompt; + } + + + /** + * @return Activity + */ + public Activity getRetryPrompt() { + return this.retryPrompt; + } + + + /** + * @param withRetryPrompt value to set the Retry property to + */ + public void setRetryPrompt(Activity withRetryPrompt) { + this.retryPrompt = withRetryPrompt; + } + + + /** + * @return List + */ + public List getChoices() { + return this.choices; + } + + + /** + * @param withChoices value to set the Choices property to + */ + public void setChoices(List withChoices) { + this.choices = withChoices; + } + + + /** + * @return ListStyle + */ + public ListStyle getStyle() { + return this.style; + } + + + /** + * @param withStyle value to set the Style property to + */ + public void setStyle(ListStyle withStyle) { + this.style = withStyle; + } + + + /** + * @return Object + */ + public Object getValidations() { + return this.validations; + } + + + /** + * @param withValidations value to set the Validations property to + */ + public void setValidations(Object withValidations) { + this.validations = withValidations; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptRecognizerResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptRecognizerResult.java new file mode 100644 index 000000000..ea9c6391c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptRecognizerResult.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +/** + * Contains the result returned by the recognition method of a {@link Prompt{T}} + * . + * + * @param The type of value the prompt returns. + */ +public class PromptRecognizerResult { + + private T value; + private Boolean succeeded; + private Boolean allowInterruption = false; + + /** + * Initializes a new instance of the {@link PromptRecognizerResult{T}} class. + */ + public PromptRecognizerResult() { + succeeded = false; + } + + /** + * Gets the recognition value. + * + * @return The recognition value. + */ + public T getValue() { + return this.value; + } + + /** + * Sets the recogntion value. + * + * @param value Value to set the recognition value to. + */ + public void setValue(T value) { + this.value = value; + } + + /** + * Gets a value indicating whether the recognition attempt succeeded. + * + * @return True if the recognition attempt succeeded; otherwise, false. + */ + public Boolean getSucceeded() { + return this.succeeded; + } + + /** + * Sets a value indicating whether the recognition attempt succeeded. + * + * @param succeeded True if the recognition attempt succeeded; otherwise, false. + */ + + public void setSucceeded(Boolean succeeded) { + this.succeeded = succeeded; + } + + /** + * Gets a value indicating whether flag indicating whether or not parent dialogs + * should be allowed to interrupt the prompt. + * + * @return The default value is `false`. + */ + public Boolean getAllowInterruption() { + return this.allowInterruption; + } + + /** + * Sets a value indicating whether flag indicating whether or not parent dialogs + * should be allowed to interrupt the prompt. + * + * @param allowInterruption The default value is `false`. + */ + public void setAllowInterruption(Boolean allowInterruption) { + this.allowInterruption = allowInterruption; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptValidator.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptValidator.java new file mode 100644 index 000000000..8fe211cbc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptValidator.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.concurrent.CompletableFuture; + +/** + * The interface definition for custom prompt validators. Implement this + * function to add custom validation to a prompt. + * + * @param Type the PromptValidator is created for. + */ + +public interface PromptValidator { + + /** + * The delegate definition for custom prompt validators. Implement this function to add custom + * validation to a prompt. + * + * @param promptContext The prompt validation context. + * + * @return A {@link CompletableFuture} of bool representing the asynchronous operation + * indicating validation success or failure. + */ + CompletableFuture promptValidator(PromptValidatorContext promptContext); + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptValidatorContext.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptValidatorContext.java new file mode 100644 index 000000000..57b95abb4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/PromptValidatorContext.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.Map; + +import com.microsoft.bot.builder.TurnContext; + +/** + * Contains context information for a {@link PromptValidator{T}} . + * + * @param Type for this Context + */ + +public class PromptValidatorContext { + + private Map state; + private PromptOptions options; + private TurnContext context; + private PromptRecognizerResult recognized; + + /** + * Create a PromptValidatorContext Instance. + * + * @param turnContext Context for the current turn of conversation with the + * user. + * @param recognized The recognition results from the prompt's recognition + * attempt. + * @param state State for the associated prompt instance. + * @param options The prompt options used for this recognition attempt. + */ + public PromptValidatorContext(TurnContext turnContext, PromptRecognizerResult recognized, + Map state, PromptOptions options) { + this.context = turnContext; + this.options = options; + this.recognized = recognized; + this.state = state; + } + + /** + * Gets state for the associated prompt instance. + * + * @return State for the associated prompt instance. + */ + public Map getState() { + return this.state; + } + + /** + * Gets the {@link PromptOptions} used for this recognition attempt. + * + * @return The prompt options used for this recognition attempt. + */ + public PromptOptions getOptions() { + return this.options; + } + + /** + * Gets the {@link TurnContext} for the current turn of conversation with the + * user. + * + * @return Context for the current turn of conversation with the user. + */ + public TurnContext getContext() { + return this.context; + } + + /** + * Gets the {@link PromptRecognizerResult{T}} returned from the prompt's + * recognition attempt. + * + * @return The recognition results from the prompt's recognition attempt. + */ + public PromptRecognizerResult getRecognized() { + return this.recognized; + } + + /** + * Gets the number of times this instance of the prompt has been executed. + * + * This count is set when the prompt is added to the dialog stack. + * + * @return the attempt count. + */ + public int getAttemptCount() { + if (!state.containsKey(Prompt.ATTEMPTCOUNTKEY)) { + return 0; + } + + return (int) state.get(Prompt.ATTEMPTCOUNTKEY); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java new file mode 100644 index 000000000..a41e04a50 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/TextPrompt.java @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogEvent; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; + +/** + * Prompts the user for text input. + */ +public class TextPrompt extends Prompt { + + /** + * Initializes a new instance of the {@link TextPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * + * The value of {@link dialogId} must be unique within the {@link DialogSet} or + * {@link ComponentDialog} to which the prompt is added. + */ + public TextPrompt(String dialogId) { + this(dialogId, null); + } + + /** + * Initializes a new instance of the {@link TextPrompt} class. + * + * @param dialogId The ID to assign to this prompt. + * @param validator Optional, a {@link PromptValidator{FoundChoice}} that contains + * additional, custom validation for this prompt. + * + * The value of {@link dialogId} must be unique within the {@link DialogSet} or + * {@link ComponentDialog} to which the prompt is added. + */ + public TextPrompt(String dialogId, PromptValidator validator) { + super(dialogId, validator); + } + + /** + * Prompts the user for input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * @param isRetry true if this is the first time this prompt dialog instance on the + * stack is prompting the user for input; otherwise, false. + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + */ + @Override + protected CompletableFuture onPrompt(TurnContext turnContext, Map state, + PromptOptions options, Boolean isRetry) { + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + if (options == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "options cannot be null" + )); + } + + if (isRetry && options.getRetryPrompt() != null) { + return turnContext.sendActivity(options.getRetryPrompt()).thenApply(result -> null); + } else if (options.getPrompt() != null) { + return turnContext.sendActivity(options.getPrompt()).thenApply(result -> null); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Attempts to recognize the user's input. + * + * @param turnContext Context for the current turn of conversation with the user. + * @param state Contains state for the current instance of the prompt on the + * dialog stack. + * @param options A prompt options Object constructed from the options initially + * provided in the call to {@link DialogContext#prompt(String, PromptOptions)} . + * + * @return A {@link CompletableFuture} representing the asynchronous operation. + * + * If the task is successful, the result describes the result of the recognition attempt. + */ + @Override + protected CompletableFuture> onRecognize(TurnContext turnContext, + Map state, PromptOptions options) { + + if (turnContext == null) { + return Async.completeExceptionally(new IllegalArgumentException( + "turnContext cannot be null" + )); + } + + PromptRecognizerResult result = new PromptRecognizerResult(); + if (turnContext.getActivity().isType(ActivityTypes.MESSAGE)) { + Activity message = turnContext.getActivity(); + if (message.getText() != null) { + result.setSucceeded(true); + result.setValue(message.getText()); + } + } + + return CompletableFuture.completedFuture(result); + } + + /** + * Called before an event is bubbled to its parent. + * + * This is a good place to perform interception of an event as returning `true` will prevent + * any further bubbling of the event to the dialogs parents and will also prevent any child + * dialogs from performing their default processing. + * + * @param dc The dialog context for the current turn of conversation. + * @param e The event being raised. + * + * @return Whether the event is handled by the current dialog and further processing + * should stop. + */ + @Override + protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) { + return CompletableFuture.completedFuture(false); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/package-info.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/package-info.java new file mode 100644 index 000000000..cdb075a12 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for com.microsoft.bot.dialogs.prompts. + */ +package com.microsoft.bot.dialogs.prompts; diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Culture.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Culture.java new file mode 100644 index 000000000..d5e0a77a4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Culture.java @@ -0,0 +1,41 @@ +package com.microsoft.recognizers.text; + +import java.util.Arrays; + +public class Culture { + public static final String English = "en-us"; + public static final String Chinese = "zh-cn"; + public static final String Spanish = "es-es"; + public static final String Portuguese = "pt-br"; + public static final String French = "fr-fr"; + public static final String German = "de-de"; + public static final String Japanese = "ja-jp"; + public static final String Dutch = "nl-nl"; + public static final String Italian = "it-it"; + + public static final Culture[] SupportedCultures = new Culture[]{ + new Culture("English", English), + new Culture("Chinese", Chinese), + new Culture("Spanish", Spanish), + new Culture("Portuguese", Portuguese), + new Culture("French", French), + new Culture("German", German), + new Culture("Japanese", Japanese), + new Culture("Dutch", Dutch), + new Culture("Italian", Italian), + }; + + public final String cultureName; + public final String cultureCode; + + public Culture(String cultureName, String cultureCode) { + this.cultureName = cultureName; + this.cultureCode = cultureCode; + } + + public static String[] getSupportedCultureCodes() { + return Arrays.stream(SupportedCultures) + .map(c -> c.cultureCode) + .toArray(String[]::new); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/CultureInfo.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/CultureInfo.java new file mode 100644 index 000000000..fb3bd8949 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/CultureInfo.java @@ -0,0 +1,14 @@ +package com.microsoft.recognizers.text; + +public class CultureInfo { + + public final String cultureCode; + + public CultureInfo(String cultureCode) { + this.cultureCode = cultureCode; + } + + public static CultureInfo getCultureInfo(String cultureCode) { + return new CultureInfo(cultureCode); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ExtendedModelResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ExtendedModelResult.java new file mode 100644 index 000000000..bc8931903 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ExtendedModelResult.java @@ -0,0 +1,19 @@ +package com.microsoft.recognizers.text; + +import java.util.SortedMap; + +public class ExtendedModelResult extends ModelResult { + // Parameter Key + public static final String ParentTextKey = "parentText"; + + public final String parentText; + + public ExtendedModelResult(String text, int start, int end, String typeName, SortedMap resolution, String parentText) { + super(text, start, end, typeName, resolution); + this.parentText = parentText; + } + + public ExtendedModelResult(ModelResult result, String parentText) { + this(result.text, result.start, result.end, result.typeName, result.resolution, parentText); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ExtractResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ExtractResult.java new file mode 100644 index 000000000..4cdae8445 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ExtractResult.java @@ -0,0 +1,103 @@ +package com.microsoft.recognizers.text; + +public class ExtractResult { + + private Integer start; + private Integer length; + private Object data; + private String type; + private String text; + private Metadata metadata; + + public ExtractResult() { + this(null, null, null, null); + } + + public ExtractResult(Integer start, Integer length, String text, String type) { + this(start, length, text, type, null, null); + } + + public ExtractResult(Integer start, Integer length, String text, String type, Object data, Metadata metadata) { + this.start = start; + this.length = length; + this.text = text; + this.type = type; + this.data = data; + this.metadata = metadata; + } + + public ExtractResult(Integer start, Integer length, String text, String type, Object data) { + this.start = start; + this.length = length; + this.text = text; + this.type = type; + this.data = data; + this.metadata = null; + } + + private boolean isOverlap(ExtractResult er1, ExtractResult er2) { + return !(er1.getStart() >= er2.getStart() + er2.getLength()) && + !(er2.getStart() >= er1.getStart() + er1.getLength()); + } + + public boolean isOverlap(ExtractResult er) { + return isOverlap(this, er); + } + + private boolean isCover(ExtractResult er1, ExtractResult er2) { + return ((er2.getStart() < er1.getStart()) && ((er2.getStart() + er2.getLength()) >= (er1.getStart() + er1.getLength()))) || + ((er2.getStart() <= er1.getStart()) && ((er2.getStart() + er2.getLength()) > (er1.getStart() + er1.getLength()))); + } + + public boolean isCover(ExtractResult er) { + return isCover(this, er); + } + + public Integer getStart() { + return start; + } + + public void setStart(Integer start) { + this.start = start; + } + + public Integer getLength() { + return length; + } + + public void setLength(Integer length) { + this.length = length; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Metadata getMetadata() { + return metadata; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IExtractor.java new file mode 100644 index 000000000..35a5e1bea --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IExtractor.java @@ -0,0 +1,7 @@ +package com.microsoft.recognizers.text; + +import java.util.List; + +public interface IExtractor { + List extract(String input); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IModel.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IModel.java new file mode 100644 index 000000000..cc529689a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IModel.java @@ -0,0 +1,10 @@ +package com.microsoft.recognizers.text; + +import java.util.List; + +public interface IModel { + String getModelTypeName(); + + List parse(String query); + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IParser.java new file mode 100644 index 000000000..45c7ce76a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/IParser.java @@ -0,0 +1,5 @@ +package com.microsoft.recognizers.text; + +public interface IParser { + ParseResult parse(ExtractResult extractResult); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Metadata.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Metadata.java new file mode 100644 index 000000000..9e89af535 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Metadata.java @@ -0,0 +1,49 @@ +package com.microsoft.recognizers.text; + +public class Metadata { + // For cases like "from 2014 to 2018", the period end "2018" could be inclusive or exclusive + // For extraction, we only mark this flag to avoid future duplicate judgment, whether to include the period end or not is not determined in the extraction step + private boolean possiblyIncludePeriodEnd = false; + + // For cases like "2015年以前" (usually regards as "before 2015" in English), "5天以前" + // (usually regards as "5 days ago" in English) in Chinese, we need to decide whether this is a "Date with Mode" or "Duration with Before and After". + // We use this flag to avoid duplicate judgment both in the Extraction step and Parse step. + // Currently, this flag is only used in Chinese DateTime as other languages don't have this ambiguity cases. + private boolean isDurationWithBeforeAndAfter = false; + + private boolean isHoliday = false; + + public boolean getIsHoliday() { + return isHoliday; + } + + public void setIsHoliday(boolean isHoliday) { + this.isHoliday = isHoliday; + } + + private boolean hasMod = false; + + public boolean getHasMod() { + return hasMod; + } + + public void setHasMod(boolean hasMod) { + this.hasMod = hasMod; + } + + public boolean getIsPossiblyIncludePeriodEnd() { + return possiblyIncludePeriodEnd; + } + + public void setPossiblyIncludePeriodEnd(boolean possiblyIncludePeriodEnd) { + this.possiblyIncludePeriodEnd = possiblyIncludePeriodEnd; + } + + public boolean getIsDurationWithBeforeAndAfter() { + return isDurationWithBeforeAndAfter; + } + + public void setDurationWithBeforeAndAfter(boolean durationWithBeforeAndAfter) { + isDurationWithBeforeAndAfter = durationWithBeforeAndAfter; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ModelFactory.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ModelFactory.java new file mode 100644 index 000000000..ef2ec6370 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ModelFactory.java @@ -0,0 +1,81 @@ +package com.microsoft.recognizers.text; + +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.javatuples.Pair; +import org.javatuples.Triplet; + +public class ModelFactory extends HashMap, Function> { + + // cacheKey: (string culture, Type modelType, string modelOptions) + private static ConcurrentHashMap, IModel> cache = new ConcurrentHashMap, IModel>(); + + private static final String fallbackCulture = Culture.English; + + public T getModel(Class modelType, String culture, boolean fallbackToDefaultCulture, TModelOptions options) throws IllegalArgumentException { + IModel model = this.getModel(modelType, culture, options); + if (model != null) { + return (T)model; + } + + if (fallbackToDefaultCulture) { + model = this.getModel(modelType, fallbackCulture, options); + if (model != null) { + return (T)model; + } + } + + throw new IllegalArgumentException( + String.format("Could not find Model with the specified configuration: %s, %s", culture, modelType.getTypeName())); + } + + private IModel getModel(Type modelType, String culture, TModelOptions options) { + if (StringUtility.isNullOrEmpty(culture)) { + return null; + } + + // Look in cache + Triplet cacheKey = new Triplet<>(culture.toLowerCase(), modelType, options.toString()); + if (cache.containsKey(cacheKey)) { + return cache.get(cacheKey); + } + + // Use Factory to create instance + Pair key = generateKey(culture, modelType); + if (this.containsKey(key)) { + Function factoryMethod = this.get(key); + IModel model = factoryMethod.apply(options); + + // Store in cache + cache.put(cacheKey, model); + return model; + } + + return null; + } + + public void initializeModels(String targetCulture, TModelOptions options) { + this.keySet().stream() + .filter(key -> StringUtility.isNullOrEmpty(targetCulture) || key.getValue0().equalsIgnoreCase(targetCulture)) + .forEach(key -> this.initializeModel(key.getValue1(), key.getValue0(), options)); + } + + private void initializeModel(Type modelType, String culture, TModelOptions options) { + this.getModel(modelType, culture, options); + } + + @Override + public Function put(Pair config, Function modelCreator) { + return super.put( + generateKey(config.getValue0(), config.getValue1()), + modelCreator); + } + + private static Pair generateKey(String culture, Type modelType) { + return new Pair<>(culture.toLowerCase(), modelType); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ModelResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ModelResult.java new file mode 100644 index 000000000..69229e121 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ModelResult.java @@ -0,0 +1,20 @@ +package com.microsoft.recognizers.text; + +import java.util.SortedMap; + +public class ModelResult { + + public final String text; + public final int start; + public final int end; + public final String typeName; + public final SortedMap resolution; + + public ModelResult(String text, int start, int end, String typeName, SortedMap resolution) { + this.text = text; + this.start = start; + this.end = end; + this.typeName = typeName; + this.resolution = resolution; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ParseResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ParseResult.java new file mode 100644 index 000000000..9a7b0a8cb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ParseResult.java @@ -0,0 +1,46 @@ +package com.microsoft.recognizers.text; + +public class ParseResult extends ExtractResult { + + // Value is for resolution. + // e.g. 1000 for "one thousand". + // The resolutions are different for different parsers. + // Therefore, we use object here. + private Object value; + + // Output the value in string format. + // It is used in some parsers. + private String resolutionStr; + + public ParseResult(Integer start, Integer length, String text, String type, Object data, Object value, String resolutionStr) { + super(start, length, text, type, data, null); + this.value = value; + this.resolutionStr = resolutionStr; + } + + public ParseResult(ExtractResult er) { + this(er.getStart(), er.getLength(), er.getText(), er.getType(), er.getData(), null, null); + } + + public ParseResult(Integer start, Integer length, String text, String type, Object data, Object value, String resolutionStr, Metadata metadata) { + super(start, length, text, type, data, metadata); + this.value = value; + this.resolutionStr = resolutionStr; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getResolutionStr() { + return resolutionStr; + } + + public void setResolutionStr(String resolutionStr) { + this.resolutionStr = resolutionStr; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Recognizer.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Recognizer.java new file mode 100644 index 000000000..e0d2253f8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/Recognizer.java @@ -0,0 +1,42 @@ +package com.microsoft.recognizers.text; + +import java.util.function.Function; +import org.javatuples.Pair; + +public abstract class Recognizer> { + + public final String targetCulture; + public final TRecognizerOptions options; + + private final ModelFactory factory; + + protected Recognizer(String targetCulture, TRecognizerOptions options, boolean lazyInitialization) { + this.targetCulture = targetCulture; + this.options = options; + + this.factory = new ModelFactory<>(); + this.initializeConfiguration(); + + if (!lazyInitialization) { + this.initializeModels(targetCulture, options); + } + } + + public T getModel(Class modelType, String culture, boolean fallbackToDefaultCulture) { + return this.factory.getModel( + modelType, + culture != null ? culture : targetCulture, + fallbackToDefaultCulture, + options); + } + + public void registerModel(Class modelType, String culture, Function modelCreator) { + this.factory.put(new Pair<>(culture, modelType), modelCreator); + } + + private void initializeModels(String targetCulture, TRecognizerOptions options) { + this.factory.initializeModels(targetCulture, options); + } + + protected abstract void initializeConfiguration(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ResolutionKey.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ResolutionKey.java new file mode 100644 index 000000000..6e14a7ee5 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/ResolutionKey.java @@ -0,0 +1,10 @@ +package com.microsoft.recognizers.text; + +public class ResolutionKey { + public static final String ValueSet = "values"; + public static final String Value = "value"; + public static final String Type = "type"; + public static final String Unit = "unit"; + public static final String Score = "score"; + public static final String IsoCurrency = "isoCurrency"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/ChoiceOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/ChoiceOptions.java new file mode 100644 index 000000000..16470e707 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/ChoiceOptions.java @@ -0,0 +1,5 @@ +package com.microsoft.recognizers.text.choice; + +public enum ChoiceOptions { + None +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/ChoiceRecognizer.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/ChoiceRecognizer.java new file mode 100644 index 000000000..782046b0f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/ChoiceRecognizer.java @@ -0,0 +1,74 @@ +package com.microsoft.recognizers.text.choice; + +import com.microsoft.recognizers.text.Culture; +import com.microsoft.recognizers.text.IModel; +import com.microsoft.recognizers.text.ModelResult; +import com.microsoft.recognizers.text.Recognizer; +import com.microsoft.recognizers.text.choice.english.extractors.EnglishBooleanExtractorConfiguration; +import com.microsoft.recognizers.text.choice.extractors.BooleanExtractor; +import com.microsoft.recognizers.text.choice.models.BooleanModel; +import com.microsoft.recognizers.text.choice.parsers.BooleanParser; + +import java.util.List; + +public class ChoiceRecognizer extends Recognizer { + + public ChoiceRecognizer(String targetCulture, ChoiceOptions options, boolean lazyInitialization) { + super(targetCulture, options, lazyInitialization); + } + + public ChoiceRecognizer(String targetCulture, int options, boolean lazyInitialization) { + this(targetCulture, ChoiceOptions.values()[options], lazyInitialization); + } + + public ChoiceRecognizer(int options, boolean lazyInitialization) { + this(null, ChoiceOptions.values()[options], lazyInitialization); + } + + public ChoiceRecognizer(ChoiceOptions options, boolean lazyInitialization) { + this(null, options, lazyInitialization); + } + + public ChoiceRecognizer(boolean lazyInitialization) { + this(null, ChoiceOptions.None, lazyInitialization); + } + + public ChoiceRecognizer(int options) { + this(null, ChoiceOptions.values()[options], true); + } + + public ChoiceRecognizer(ChoiceOptions options) { + this(null, options, true); + } + + public ChoiceRecognizer() { + this(null, ChoiceOptions.None, true); + } + + public BooleanModel getBooleanModel(String culture, boolean fallbackToDefaultCulture) { + return getModel(BooleanModel.class, culture, fallbackToDefaultCulture); + } + + public static List recognizeBoolean(String query, String culture, ChoiceOptions options, boolean fallbackToDefaultCulture) { + + ChoiceRecognizer recognizer = new ChoiceRecognizer(options); + IModel model = recognizer.getBooleanModel(culture, fallbackToDefaultCulture); + + return model.parse(query); + } + + public static List recognizeBoolean(String query, String culture, ChoiceOptions options) { + return recognizeBoolean(query, culture, options, true); + } + + public static List recognizeBoolean(String query, String culture) { + return recognizeBoolean(query, culture, ChoiceOptions.None); + } + + @Override + protected void initializeConfiguration() { + + //English + registerModel(BooleanModel.class, Culture.English, (options) -> new BooleanModel(new BooleanParser(), new BooleanExtractor(new EnglishBooleanExtractorConfiguration()))); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/Constants.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/Constants.java new file mode 100644 index 000000000..3db27c8dc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/Constants.java @@ -0,0 +1,8 @@ +package com.microsoft.recognizers.text.choice; + +public class Constants { + public static final String SYS_BOOLEAN_TRUE = "boolean_true"; + public static final String SYS_BOOLEAN_FALSE = "boolean_false"; + // Model type name + public static final String MODEL_BOOLEAN = "boolean"; +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/config/BooleanParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/config/BooleanParserConfiguration.java new file mode 100644 index 000000000..e19e35402 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/config/BooleanParserConfiguration.java @@ -0,0 +1,19 @@ +package com.microsoft.recognizers.text.choice.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.choice.Constants; + +import java.util.Map; + +public class BooleanParserConfiguration implements IChoiceParserConfiguration { + + public static Map Resolutions = ImmutableMap.builder() + .put(Constants.SYS_BOOLEAN_TRUE, true) + .put(Constants.SYS_BOOLEAN_FALSE, false) + .build(); + + @Override + public Map getResolutions() { + return Resolutions; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/config/IChoiceParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/config/IChoiceParserConfiguration.java new file mode 100644 index 000000000..d56112251 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/config/IChoiceParserConfiguration.java @@ -0,0 +1,7 @@ +package com.microsoft.recognizers.text.choice.config; + +import java.util.Map; + +public interface IChoiceParserConfiguration { + public Map getResolutions(); +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/english/extractors/EnglishBooleanExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/english/extractors/EnglishBooleanExtractorConfiguration.java new file mode 100644 index 000000000..8b2c4ab65 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/english/extractors/EnglishBooleanExtractorConfiguration.java @@ -0,0 +1,70 @@ +package com.microsoft.recognizers.text.choice.english.extractors; + +import com.microsoft.recognizers.text.choice.Constants; +import com.microsoft.recognizers.text.choice.extractors.IBooleanExtractorConfiguration; +import com.microsoft.recognizers.text.choice.resources.EnglishChoice; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +public class EnglishBooleanExtractorConfiguration implements IBooleanExtractorConfiguration { + public static final Pattern trueRegex = RegExpUtility.getSafeRegExp(EnglishChoice.TrueRegex); + public static final Pattern falseRegex = RegExpUtility.getSafeRegExp(EnglishChoice.FalseRegex); + public static final Pattern tokenRegex = RegExpUtility.getSafeRegExp(EnglishChoice.TokenizerRegex); + public static final Map mapRegexes; + + static { + mapRegexes = new HashMap(); + mapRegexes.put(trueRegex, Constants.SYS_BOOLEAN_TRUE); + mapRegexes.put(falseRegex, Constants.SYS_BOOLEAN_FALSE); + } + + public boolean allowPartialMatch = false; + public int maxDistance = 2; + public boolean onlyTopMatch; + + public EnglishBooleanExtractorConfiguration(boolean topMatch) { + onlyTopMatch = topMatch; + } + + public EnglishBooleanExtractorConfiguration() { + this(true); + } + + @Override + public Map getMapRegexes() { + return mapRegexes; + } + + @Override + public Pattern getTrueRegex() { + return trueRegex; + } + + @Override + public Pattern getFalseRegex() { + return falseRegex; + } + + @Override + public Pattern getTokenRegex() { + return tokenRegex; + } + + @Override + public boolean getAllowPartialMatch() { + return allowPartialMatch; + } + + @Override + public int getMaxDistance() { + return maxDistance; + } + + @Override + public boolean getOnlyTopMatch() { + return onlyTopMatch; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/BooleanExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/BooleanExtractor.java new file mode 100644 index 000000000..6f9b8036f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/BooleanExtractor.java @@ -0,0 +1,9 @@ +package com.microsoft.recognizers.text.choice.extractors; + +public class BooleanExtractor extends ChoiceExtractor { + + public BooleanExtractor(IBooleanExtractorConfiguration config) { + + super(config); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/ChoiceExtractDataResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/ChoiceExtractDataResult.java new file mode 100644 index 000000000..7fd89481a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/ChoiceExtractDataResult.java @@ -0,0 +1,19 @@ +package com.microsoft.recognizers.text.choice.extractors; + +import com.microsoft.recognizers.text.ExtractResult; + +import java.util.ArrayList; +import java.util.List; + +public class ChoiceExtractDataResult { + + public final List otherMatches; + public final String source; + public final double score; + + public ChoiceExtractDataResult(String extractDataSource, double extractDataScore, List extractDataOtherMatches) { + otherMatches = extractDataOtherMatches; + source = extractDataSource; + score = extractDataScore; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/ChoiceExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/ChoiceExtractor.java new file mode 100644 index 000000000..55f351bb9 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/ChoiceExtractor.java @@ -0,0 +1,170 @@ +package com.microsoft.recognizers.text.choice.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.choice.utilities.UnicodeUtils; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +public class ChoiceExtractor implements IExtractor { + + private IChoiceExtractorConfiguration config; + + public ChoiceExtractor(IChoiceExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String text) { + + List results = new ArrayList<>(); + String trimmedText = text.toLowerCase(); + List partialResults = new ArrayList<>(); + List sourceTokens = tokenize(trimmedText); + + if (text.isEmpty()) { + return results; + } + + for (Map.Entry entry : this.config.getMapRegexes().entrySet()) { + + Pattern regexKey = entry.getKey(); + String constantValue = entry.getValue(); + Match[] matches = RegExpUtility.getMatches(regexKey, trimmedText); + double topScore = 0; + + for (Match match : matches) { + + List matchToken = tokenize(match.value); + for (int i = 0; i < sourceTokens.size(); i++) { + double score = matchValue(sourceTokens, matchToken, i); + topScore = Math.max(topScore, score); + } + + if (topScore > 0.0) { + int start = match.index; + int length = match.length; + partialResults.add( + new ExtractResult( + start, + length, + text.substring(start, length + start), + constantValue, + new ChoiceExtractDataResult(text, topScore, new ArrayList<>()) + ) + ); + } + + } + } + + if (partialResults.size() == 0) { + return results; + } + + partialResults.sort(Comparator.comparingInt(er -> er.getStart())); + + if (this.config.getOnlyTopMatch()) { + + double topScore = 0; + int topResultIndex = 0; + + for (int i = 0; i < partialResults.size(); i++) { + + ChoiceExtractDataResult data = (ChoiceExtractDataResult)partialResults.get(i).getData(); + if (data.score > topScore) { + topScore = data.score; + topResultIndex = i; + } + + } + results.add(partialResults.get(topResultIndex)); + partialResults.remove(topResultIndex); + } else { + results = partialResults; + } + + return results; + } + + private final double matchValue(List source, List match, int startPosition) { + + double matched = 0; + double totalDeviation = 0; + double score = 0; + + for (String token : match) { + int pos = indexOfToken(source, token, startPosition); + if (pos >= 0) { + int distance = matched > 0 ? pos - startPosition : 0; + if (distance <= config.getMaxDistance()) { + matched++; + totalDeviation += distance; + startPosition = pos + 1; + } + } + } + + if (matched > 0 && (matched == match.size() || config.getAllowPartialMatch())) { + double completeness = matched / match.size(); + double accuracy = completeness * (matched / (matched + totalDeviation)); + double initialScore = accuracy * (matched / source.size()); + score = 0.4 + (0.6 * initialScore); + } + + return score; + } + + private static int indexOfToken(List tokens, String token, int startPos) { + + if (tokens.size() <= startPos) { + return -1; + } + + return tokens.indexOf(token); + } + + private final List tokenize(String text) { + + List tokens = new ArrayList<>(); + List letters = UnicodeUtils.letters(text); + String token = ""; + + for (String letter : letters) { + + Optional isMatch = Arrays.stream(RegExpUtility.getMatches(this.config.getTokenRegex(), letter)).findFirst(); + if (UnicodeUtils.isEmoji(letter)) { + + // Character is in a Supplementary Unicode Plane. This is where emoji live so + // we're going to just break each character in this range out as its own token. + tokens.add(letter); + if (!StringUtility.isNullOrWhiteSpace(token)) { + tokens.add(token); + token = ""; + } + + } else if (!(isMatch.isPresent() || StringUtility.isNullOrWhiteSpace(letter))) { + token = token + letter; + } else if (!StringUtility.isNullOrWhiteSpace(token)) { + tokens.add(token); + token = ""; + } + } + + if (!StringUtility.isNullOrWhiteSpace(token)) { + tokens.add(token); + token = ""; + } + + return tokens; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/IBooleanExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/IBooleanExtractorConfiguration.java new file mode 100644 index 000000000..fbcbe8e9a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/IBooleanExtractorConfiguration.java @@ -0,0 +1,9 @@ +package com.microsoft.recognizers.text.choice.extractors; + +import java.util.regex.Pattern; + +public interface IBooleanExtractorConfiguration extends IChoiceExtractorConfiguration { + public Pattern getTrueRegex(); + + public Pattern getFalseRegex(); +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/IChoiceExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/IChoiceExtractorConfiguration.java new file mode 100644 index 000000000..609a3b0d4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/extractors/IChoiceExtractorConfiguration.java @@ -0,0 +1,16 @@ +package com.microsoft.recognizers.text.choice.extractors; + +import java.util.Map; +import java.util.regex.Pattern; + +public interface IChoiceExtractorConfiguration { + public Map getMapRegexes(); + + public Pattern getTokenRegex(); + + public boolean getAllowPartialMatch(); + + public int getMaxDistance(); + + public boolean getOnlyTopMatch(); +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/models/BooleanModel.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/models/BooleanModel.java new file mode 100644 index 000000000..9c696f446 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/models/BooleanModel.java @@ -0,0 +1,43 @@ +package com.microsoft.recognizers.text.choice.models; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.choice.Constants; +import com.microsoft.recognizers.text.choice.parsers.OptionsOtherMatchParseResult; +import com.microsoft.recognizers.text.choice.parsers.OptionsParseDataResult; + +import java.util.SortedMap; +import java.util.TreeMap; + +public class BooleanModel extends ChoiceModel { + + public BooleanModel(IParser parser, IExtractor extractor) { + super(parser, extractor); + } + + public String getModelTypeName() { + return Constants.MODEL_BOOLEAN; + } + + @Override + protected SortedMap getResolution(ParseResult parseResult) { + + OptionsParseDataResult parseResultData = (OptionsParseDataResult)parseResult.getData(); + SortedMap results = new TreeMap(); + SortedMap otherMatchesMap = new TreeMap(); + + results.put("value", parseResult.getValue()); + results.put("score", parseResultData.score); + + for (OptionsOtherMatchParseResult otherMatchParseRes : parseResultData.otherMatches) { + otherMatchesMap.put("text", otherMatchParseRes.text); + otherMatchesMap.put("value", otherMatchParseRes.value); + otherMatchesMap.put("score", otherMatchParseRes.score); + } + + results.put("otherResults", otherMatchesMap); + + return results; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/models/ChoiceModel.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/models/ChoiceModel.java new file mode 100644 index 000000000..516bb121c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/models/ChoiceModel.java @@ -0,0 +1,43 @@ +package com.microsoft.recognizers.text.choice.models; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IModel; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.ModelResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.choice.Constants; + +import java.util.List; +import java.util.SortedMap; +import java.util.stream.Collectors; + +public abstract class ChoiceModel implements IModel { + protected IExtractor extractor; + protected IParser parser; + + public ChoiceModel(IParser choiceParser, IExtractor choiceExtractor) { + parser = choiceParser; + extractor = choiceExtractor; + } + + @Override + public String getModelTypeName() { + return Constants.MODEL_BOOLEAN; + } + + @Override + public List parse(String query) { + + List extractResults = extractor.extract(query); + List parseResults = extractResults.stream().map(exRes -> parser.parse(exRes)).collect(Collectors.toList()); + + List modelResults = parseResults.stream().map( + parseRes -> new ModelResult(parseRes.getText(), parseRes.getStart(), parseRes.getStart() + parseRes.getLength() - 1, getModelTypeName(), getResolution(parseRes)) + ).collect(Collectors.toList()); + + return modelResults; + } + + protected abstract SortedMap getResolution(ParseResult parseResult); +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/BooleanParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/BooleanParser.java new file mode 100644 index 000000000..f0ce82382 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/BooleanParser.java @@ -0,0 +1,9 @@ +package com.microsoft.recognizers.text.choice.parsers; + +import com.microsoft.recognizers.text.choice.config.BooleanParserConfiguration; + +public class BooleanParser extends ChoiceParser { + public BooleanParser() { + super(new BooleanParserConfiguration()); + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/ChoiceParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/ChoiceParser.java new file mode 100644 index 000000000..91316e36a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/ChoiceParser.java @@ -0,0 +1,43 @@ +package com.microsoft.recognizers.text.choice.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.choice.config.IChoiceParserConfiguration; +import com.microsoft.recognizers.text.choice.extractors.ChoiceExtractDataResult; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ChoiceParser implements IParser { + + private IChoiceParserConfiguration config; + + public ChoiceParser(IChoiceParserConfiguration config) { + this.config = config; + } + + public ParseResult parse(ExtractResult extractResult) { + + ParseResult parseResult = new ParseResult(extractResult); + ChoiceExtractDataResult data = (ChoiceExtractDataResult)extractResult.getData(); + Map resolutions = this.config.getResolutions(); + List matches = data.otherMatches.stream().map(match -> getOptionsOtherMatchResult(match)).collect(Collectors.toList()); + + parseResult.setData(new OptionsParseDataResult(data.score, matches)); + parseResult.setValue(resolutions.getOrDefault(parseResult.getType(), false)); + + return parseResult; + } + + private OptionsOtherMatchParseResult getOptionsOtherMatchResult(ExtractResult extractResult) { + + ParseResult parseResult = new ParseResult(extractResult); + ChoiceExtractDataResult data = (ChoiceExtractDataResult)extractResult.getData(); + Map resolutions = this.config.getResolutions(); + OptionsOtherMatchParseResult result = new OptionsOtherMatchParseResult(parseResult.getText(), resolutions.get(parseResult.getType()), data.score); + + return result; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/OptionsOtherMatchParseResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/OptionsOtherMatchParseResult.java new file mode 100644 index 000000000..dd66f24b8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/OptionsOtherMatchParseResult.java @@ -0,0 +1,14 @@ +package com.microsoft.recognizers.text.choice.parsers; + +public class OptionsOtherMatchParseResult { + + public final double score; + public final String text; + public final Object value; + + public OptionsOtherMatchParseResult(String parseResultText, Object parseResultValue, double parseResultScore) { + score = parseResultScore; + text = parseResultText; + value = parseResultValue; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/OptionsParseDataResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/OptionsParseDataResult.java new file mode 100644 index 000000000..bd08beaa8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/parsers/OptionsParseDataResult.java @@ -0,0 +1,15 @@ +package com.microsoft.recognizers.text.choice.parsers; + +import java.util.ArrayList; +import java.util.List; + +public class OptionsParseDataResult { + + public final double score; + public final List otherMatches; + + public OptionsParseDataResult(double optionScore, List optionOtherMatches) { + score = optionScore; + otherMatches = optionOtherMatches; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/ChineseChoice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/ChineseChoice.java new file mode 100644 index 000000000..e7cba0bb0 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/ChineseChoice.java @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.choice.resources; + +public class ChineseChoice { + + public static final String LangMarker = "Chs"; + + public static final String TokenizerRegex = "[^\\u3040-\\u30ff\\u3400-\\u4dbf\\u4e00-\\u9fff\\uf900-\\ufaff\\uff66-\\uff9f]"; + + public static final String SkinToneRegex = "(\\uD83C\\uDFFB|\\uD83C\\uDFFC|\\uD83C\\uDFFD|\\uD83C\\uDFFE|\\uD83C\\uDFFF)"; + + public static final String TrueRegex = "(好[的啊呀嘞哇]|没问题|可以|中|好|同意|行|是的|是|对)|(\\uD83D\\uDC4D|\\uD83D\\uDC4C){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); + + public static final String FalseRegex = "(不行|不好|拒绝|否定|不中|不可以|不是的|不是|不对|不)|(\\uD83D\\uDC4E|\\u270B|\\uD83D\\uDD90){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/EnglishChoice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/EnglishChoice.java new file mode 100644 index 000000000..82d06783b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/EnglishChoice.java @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.choice.resources; + +public class EnglishChoice { + + public static final String LangMarker = "Eng"; + + public static final String TokenizerRegex = "[^\\w\\d]"; + + public static final String SkinToneRegex = "(\\uD83C\\uDFFB|\\uD83C\\uDFFC|\\uD83C\\uDFFD|\\uD83C\\uDFFE|\\uD83C\\uDFFF)"; + + public static final String TrueRegex = "\\b(true|yes|yep|yup|yeah|y|sure|ok|agree)\\b|(\\uD83D\\uDC4D|\\uD83D\\uDC4C|\\u0001f44c){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); + + public static final String FalseRegex = "\\b(false|nope|nop|no|not\\s+ok|disagree)\\b|(\\uD83D\\uDC4E|\\u270B|\\uD83D\\uDD90|\\u0001F44E|\\u0001F590){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/FrenchChoice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/FrenchChoice.java new file mode 100644 index 000000000..2f3e0104d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/FrenchChoice.java @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.choice.resources; + +public class FrenchChoice { + + public static final String LangMarker = "Fr"; + + public static final String TokenizerRegex = "[^\\w\\d\\u00E0-\\u00FC]"; + + public static final String SkinToneRegex = "(\\uD83C\\uDFFB|\\uD83C\\uDFFC|\\uD83C\\uDFFD|\\uD83C\\uDFFE|\\uD83C\\uDFFF)"; + + public static final String TrueRegex = "\\b(s[uû]r|ouais|oui|yep|y|sure|approuver|accepter|consentir|d'accord|ça march[eé])\\b|(\\uD83D\\uDC4D|\\uD83D\\uDC4C){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); + + public static final String FalseRegex = "\\b(faux|nan|non|pas\\s+d'accord|pas\\s+concorder|n'est\\s+pas\\s+(correct|ok)|pas)\\b|(\\uD83D\\uDC4E|\\u270B|\\uD83D\\uDD90){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/PortugueseChoice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/PortugueseChoice.java new file mode 100644 index 000000000..8a33849f3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/PortugueseChoice.java @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.choice.resources; + +public class PortugueseChoice { + + public static final String LangMarker = "Por"; + + public static final String TokenizerRegex = "[^\\w\\d\\u00E0-\\u00FC]"; + + public static final String SkinToneRegex = "(\\uD83C\\uDFFB|\\uD83C\\uDFFC|\\uD83C\\uDFFD|\\uD83C\\uDFFE|\\uD83C\\uDFFF)"; + + public static final String TrueRegex = "\\b(verdade|verdadeir[oa]|sim|isso|claro|ok)\\b|(\\uD83D\\uDC4D|\\uD83D\\uDC4C){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); + + public static final String FalseRegex = "\\b(falso|n[aã]o|incorreto|nada disso)\\b|(\\uD83D\\uDC4E|\\u270B|\\uD83D\\uDD90){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/SpanishChoice.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/SpanishChoice.java new file mode 100644 index 000000000..2752bcb4a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/resources/SpanishChoice.java @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.choice.resources; + +public class SpanishChoice { + + public static final String LangMarker = "Spa"; + + public static final String TokenizerRegex = "[^\\w\\d\\u00E0-\\u00FC]"; + + public static final String SkinToneRegex = "(\\uD83C\\uDFFB|\\uD83C\\uDFFC|\\uD83C\\uDFFD|\\uD83C\\uDFFE|\\uD83C\\uDFFF)"; + + public static final String TrueRegex = "\\b(verdad|verdadero|sí|sip|s|si|cierto|por supuesto|ok)\\b|(\\uD83D\\uDC4D|\\uD83D\\uDC4C){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); + + public static final String FalseRegex = "\\b(falso|no|nop|n|no)\\b|(\\uD83D\\uDC4E|\\u270B|\\uD83D\\uDD90){SkinToneRegex}?" + .replace("{SkinToneRegex}", SkinToneRegex); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/utilities/UnicodeUtils.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/utilities/UnicodeUtils.java new file mode 100644 index 000000000..fe7a281a4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/choice/utilities/UnicodeUtils.java @@ -0,0 +1,29 @@ +package com.microsoft.recognizers.text.choice.utilities; + +import java.lang.Character; +import java.util.ArrayList; +import java.util.List; + +public class UnicodeUtils { + public static boolean isEmoji(String letter) { + final int WhereEmojiLive = 0xFFFF; // Supplementary Unicode Plane. This is where emoji live + return Character.isHighSurrogate(letter.charAt(0)) && (letter.codePoints().sum() > WhereEmojiLive); + } + + public static List letters(String text) { + char codePoint = 0; + List result = new ArrayList<>(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (codePoint != 0) { + result.add(new String(Character.toChars(codePoint + c))); + codePoint = 0; + } else if (!Character.isHighSurrogate(c)) { + result.add(Character.toString(c)); + } else { + codePoint = c; + } + } + return result; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/Constants.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/Constants.java new file mode 100644 index 000000000..5046497d0 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/Constants.java @@ -0,0 +1,182 @@ +package com.microsoft.recognizers.text.datetime; + +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; + +public class Constants { + + public static final String SYS_DATETIME_DATE = "date"; + public static final String SYS_DATETIME_TIME = "time"; + public static final String SYS_DATETIME_DATEPERIOD = "daterange"; + public static final String SYS_DATETIME_DATETIME = "datetime"; + public static final String SYS_DATETIME_TIMEPERIOD = "timerange"; + public static final String SYS_DATETIME_DATETIMEPERIOD = "datetimerange"; + public static final String SYS_DATETIME_DURATION = "duration"; + public static final String SYS_DATETIME_SET = "set"; + public static final String SYS_DATETIME_DATETIMEALT = "datetimealt"; + public static final String SYS_DATETIME_TIMEZONE = "timezone"; + + // SourceEntity Types + public static final String SYS_DATETIME_DATETIMEPOINT = "datetimepoint"; + + // Model Name + public static final String MODEL_DATETIME = "datetime"; + + // Multiple Duration Types + public static final String MultipleDuration_Prefix = "multipleDuration"; + public static final String MultipleDuration_Type = MultipleDuration_Prefix + "Type"; + public static final String MultipleDuration_DateTime = MultipleDuration_Prefix + "DateTime"; + public static final String MultipleDuration_Date = MultipleDuration_Prefix + "Date"; + public static final String MultipleDuration_Time = MultipleDuration_Prefix + "Time"; + + // DateTime Parse + public static final String Resolve = "resolve"; + public static final String ResolveToPast = "resolveToPast"; + public static final String ResolveToFuture = "resolveToFuture"; + public static final String FutureDate = "futureDate"; + public static final String PastDate = "pastDate"; + public static final String ParseResult1 = "parseResult1"; + public static final String ParseResult2 = "parseResult2"; + + // In the ExtractResult data + public static final String Context = "context"; + public static final String ContextType_RelativePrefix = "relativePrefix"; + public static final String ContextType_RelativeSuffix = "relativeSuffix"; + public static final String ContextType_AmPm = "AmPm"; + public static final String SubType = "subType"; + + // Comment - internal tag used during entity processing, never exposed to users. + // Tags are filtered out in BaseMergedDateTimeParser DateTimeResolution() + public static final String Comment = "Comment"; + // AmPm time representation for time parser + public static final String Comment_AmPm = "ampm"; + // Prefix early/late for time parser + public static final String Comment_Early = "early"; + public static final String Comment_Late = "late"; + // Parse week of date format + public static final String Comment_WeekOf = "WeekOf"; + public static final String Comment_MonthOf = "MonthOf"; + + public static final String Comment_DoubleTimex = "doubleTimex"; + + public static final String InvalidDateString = "0001-01-01"; + public static final String CompositeTimexDelimiter = "|"; + public static final String CompositeTimexSplit = "\\|"; + + // Mod Value + // "before" -> To mean "preceding in time". I.e. Does not include the extracted datetime entity in the resolution's ending point. Equivalent to "<" + public static final String BEFORE_MOD = "before"; + + // "after" -> To mean "following in time". I.e. Does not include the extracted datetime entity in the resolution's starting point. Equivalent to ">" + public static final String AFTER_MOD = "after"; + + // "since" -> Same as "after", but including the extracted datetime entity. Equivalent to ">=" + public static final String SINCE_MOD = "since"; + + // "until" -> Same as "before", but including the extracted datetime entity. Equivalent to "<=" + public static final String UNTIL_MOD = "until"; + + public static final String EARLY_MOD = "start"; + public static final String MID_MOD = "mid"; + public static final String LATE_MOD = "end"; + + public static final String MORE_THAN_MOD = "more"; + public static final String LESS_THAN_MOD = "less"; + + public static final String REF_UNDEF_MOD = "ref_undef"; + + public static final String APPROX_MOD = "approx"; + + // Invalid year + public static final int InvalidYear = Integer.MIN_VALUE; + public static final int InvalidMonth = Integer.MIN_VALUE; + public static final int InvalidDay = Integer.MIN_VALUE; + public static final int InvalidHour = Integer.MIN_VALUE; + public static final int InvalidMinute = Integer.MIN_VALUE; + public static final int InvalidSecond = Integer.MIN_VALUE; + + public static final int MinYearNum = BaseDateTime.MinYearNum; + public static final int MaxYearNum = BaseDateTime.MaxYearNum; + + public static final int MaxTwoDigitYearFutureNum = BaseDateTime.MaxTwoDigitYearFutureNum; + public static final int MinTwoDigitYearPastNum = BaseDateTime.MinTwoDigitYearPastNum; + + // These are some particular values for timezone recognition + public static final int InvalidOffsetValue = -10000; + public static final String UtcOffsetMinsKey = "utcOffsetMins"; + public static final String TimeZoneText = "timezoneText"; + public static final String TimeZone = "timezone"; + public static final String ResolveTimeZone = "resolveTimeZone"; + public static final int PositiveSign = 1; + public static final int NegativeSign = -1; + + public static final int TrimesterMonthCount = 3; + public static final int QuarterCount = 4; + public static final int SemesterMonthCount = 6; + public static final int WeekDayCount = 7; + public static final int CenturyYearsCount = 100; + public static final int MaxWeekOfMonth = 5; + + // hours of one half day + public static final int HalfDayHourCount = 12; + // hours of a half mid-day-duration + public static final int HalfMidDayDurationHourCount = 2; + + // the length of four digits year, e.g., 2018 + public static final int FourDigitsYearLength = 4; + + // specifies the priority interpreting month and day order + public static final String DefaultLanguageFallback_MDY = "MDY"; + public static final String DefaultLanguageFallback_DMY = "DMY"; + public static final String DefaultLanguageFallback_YMD = "YMD"; // ZH + + // Groups' names for named groups in regexes + public static final String NextGroupName = "next"; + public static final String AmGroupName = "am"; + public static final String PmGroupName = "pm"; + public static final String ImplicitAmGroupName = "iam"; + public static final String ImplicitPmGroupName = "ipm"; + public static final String PrefixGroupName = "prefix"; + public static final String SuffixGroupName = "suffix"; + public static final String DescGroupName = "desc"; + public static final String SecondGroupName = "sec"; + public static final String MinuteGroupName = "min"; + public static final String HourGroupName = "hour"; + public static final String TimeOfDayGroupName = "timeOfDay"; + public static final String BusinessDayGroupName = "business"; + public static final String LeftAmPmGroupName = "leftDesc"; + public static final String RightAmPmGroupName = "rightDesc"; + + public static final String DECADE_UNIT = "10Y"; + public static final String FORTNIGHT_UNIT = "2W"; + + // Timex + public static final String[] DatePeriodTimexSplitter = { ",", "(", ")" }; + public static final String TimexYear = "Y"; + public static final String TimexMonth = "M"; + public static final String TimexMonthFull = "MON"; + public static final String TimexWeek = "W"; + public static final String TimexDay = "D"; + public static final String TimexBusinessDay = "BD"; + public static final String TimexWeekend = "WE"; + public static final String TimexHour = "H"; + public static final String TimexMinute = "M"; + public static final String TimexSecond = "S"; + public static final char TimexFuzzy = 'X'; + public static final String TimexFuzzyYear = "XXXX"; + public static final String TimexFuzzyMonth = "XX"; + public static final String TimexFuzzyWeek = "WXX"; + public static final String TimexFuzzyDay = "XX"; + public static final String DateTimexConnector = "-"; + public static final String TimeTimexConnector = ":"; + public static final String GeneralPeriodPrefix = "P"; + public static final String TimeTimexPrefix = "T"; + + // Timex of TimeOfDay + public static final String EarlyMorning = "TDA"; + public static final String Morning = "TMO"; + public static final String Afternoon = "TAF"; + public static final String Evening = "TEV"; + public static final String Daytime = "TDT"; + public static final String Night = "TNI"; + public static final String BusinessHour = "TBH"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DatePeriodTimexType.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DatePeriodTimexType.java new file mode 100644 index 000000000..853934357 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DatePeriodTimexType.java @@ -0,0 +1,8 @@ +package com.microsoft.recognizers.text.datetime; + +public enum DatePeriodTimexType { + ByDay, + ByWeek, + ByMonth, + ByYear +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeOptions.java new file mode 100644 index 000000000..5519bd005 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeOptions.java @@ -0,0 +1,26 @@ +package com.microsoft.recognizers.text.datetime; + +public enum DateTimeOptions { + None(0), + SkipFromToMerge(1), + SplitDateAndTime(2), + CalendarMode(4), + ExtendedTypes(8), + EnablePreview(8388608), + ExperimentalMode(4194304), + ComplexCalendar(8 + 4 + 8388608); + + private final int value; + + DateTimeOptions(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public boolean match(DateTimeOptions option) { + return (this.value & option.value) == option.value; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeRecognizer.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeRecognizer.java new file mode 100644 index 000000000..9d4c0adf1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeRecognizer.java @@ -0,0 +1,93 @@ +package com.microsoft.recognizers.text.datetime; + +import com.microsoft.recognizers.text.Culture; +import com.microsoft.recognizers.text.ModelResult; +import com.microsoft.recognizers.text.Recognizer; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.parsers.EnglishMergedParserConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseMergedDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.parsers.FrenchMergedParserConfiguration; +import com.microsoft.recognizers.text.datetime.models.DateTimeModel; +import com.microsoft.recognizers.text.datetime.parsers.BaseMergedDateTimeParser; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.parsers.SpanishMergedParserConfiguration; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Function; + +public class DateTimeRecognizer extends Recognizer { + + public DateTimeRecognizer() { + this(null, DateTimeOptions.None, true); + } + + public DateTimeRecognizer(String culture) { + this(culture, DateTimeOptions.None, false); + } + + public DateTimeRecognizer(DateTimeOptions options) { + this(null, options, true); + } + + public DateTimeRecognizer(DateTimeOptions options, boolean lazyInitialization) { + this(null, options, lazyInitialization); + } + + public DateTimeRecognizer(String culture, DateTimeOptions options, boolean lazyInitialization) { + super(culture, options, lazyInitialization); + } + + public DateTimeModel getDateTimeModel() { + return getDateTimeModel(null, true); + } + + public DateTimeModel getDateTimeModel(String culture, boolean fallbackToDefaultCulture) { + return getModel(DateTimeModel.class, culture, fallbackToDefaultCulture); + } + + //region Helper methods for less verbosity + public static List recognizeDateTime(String query, String culture) { + return recognizeByModel(recognizer -> recognizer.getDateTimeModel(culture, true), query, DateTimeOptions.None, LocalDateTime.now()); + } + + public static List recognizeDateTime(String query, String culture, DateTimeOptions options) { + return recognizeByModel(recognizer -> recognizer.getDateTimeModel(culture, true), query, options, LocalDateTime.now()); + } + + public static List recognizeDateTime(String query, String culture, DateTimeOptions options, boolean fallbackToDefaultCulture) { + return recognizeByModel(recognizer -> recognizer.getDateTimeModel(culture, fallbackToDefaultCulture), query, options, LocalDateTime.now()); + } + + public static List recognizeDateTime(String query, String culture, DateTimeOptions options, boolean fallbackToDefaultCulture, LocalDateTime reference) { + return recognizeByModel(recognizer -> recognizer.getDateTimeModel(culture, fallbackToDefaultCulture), query, options, reference); + } + //endregion + + private static List recognizeByModel(Function getModelFun, String query, DateTimeOptions options, LocalDateTime reference) { + DateTimeRecognizer recognizer = new DateTimeRecognizer(options); + DateTimeModel model = getModelFun.apply(recognizer); + return model.parse(query, reference); + } + + @Override + protected void initializeConfiguration() { + // English + registerModel(DateTimeModel.class, Culture.English, + (options) -> new DateTimeModel( + new BaseMergedDateTimeParser(new EnglishMergedParserConfiguration(options)), + new BaseMergedDateTimeExtractor(new EnglishMergedExtractorConfiguration(options)))); + + // Spanish + registerModel(DateTimeModel.class, Culture.Spanish, + (options) -> new DateTimeModel( + new BaseMergedDateTimeParser(new SpanishMergedParserConfiguration(options)), + new BaseMergedDateTimeExtractor(new SpanishMergedExtractorConfiguration(options)))); + + registerModel(DateTimeModel.class, Culture.French, dateTimeOptions -> new DateTimeModel( + new BaseMergedDateTimeParser(new FrenchMergedParserConfiguration(options)), + new BaseMergedDateTimeExtractor(new FrenchMergedExtractorConfiguration(options)) + )); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeResolutionKey.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeResolutionKey.java new file mode 100644 index 000000000..e29f7c08f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/DateTimeResolutionKey.java @@ -0,0 +1,11 @@ +package com.microsoft.recognizers.text.datetime; + +public class DateTimeResolutionKey { + public static final String Timex = "timex"; + public static final String Mod = "Mod"; + public static final String IsLunar = "isLunar"; + public static final String START = "start"; + public static final String END = "end"; + public static final String List = "list"; + public static final String SourceEntity = "sourceEntity"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/TimeTypeConstants.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/TimeTypeConstants.java new file mode 100644 index 000000000..914b8b3a7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/TimeTypeConstants.java @@ -0,0 +1,18 @@ +package com.microsoft.recognizers.text.datetime; + +public class TimeTypeConstants { + public static final String DATE = "date"; + public static final String DATETIME = "dateTime"; + public static final String DATETIMEALT = "dateTimeAlt"; + public static final String DURATION = "duration"; + public static final String SET = "set"; + public static final String TIME = "time"; + + // Internal SubType for Future/Past in DateTimeResolutionResult + public static final String START_DATE = "startDate"; + public static final String END_DATE = "endDate"; + public static final String START_DATETIME = "startDateTime"; + public static final String END_DATETIME = "endDateTime"; + public static final String START_TIME = "startTime"; + public static final String END_TIME = "endTime"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/config/BaseOptionsConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/config/BaseOptionsConfiguration.java new file mode 100644 index 000000000..36fb5ac88 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/config/BaseOptionsConfiguration.java @@ -0,0 +1,31 @@ +package com.microsoft.recognizers.text.datetime.config; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; + +public class BaseOptionsConfiguration implements IOptionsConfiguration { + private final DateTimeOptions options; + private final boolean dmyDateFormat; + + public BaseOptionsConfiguration() { + this(DateTimeOptions.None, false); + } + + public BaseOptionsConfiguration(DateTimeOptions options) { + this(options, false); + } + + public BaseOptionsConfiguration(DateTimeOptions options, boolean dmyDateFormat) { + this.options = options; + this.dmyDateFormat = dmyDateFormat; + } + + @Override + public DateTimeOptions getOptions() { + return options; + } + + @Override + public boolean getDmyDateFormat() { + return dmyDateFormat; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/config/IOptionsConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/config/IOptionsConfiguration.java new file mode 100644 index 000000000..45d3f3f0c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/config/IOptionsConfiguration.java @@ -0,0 +1,10 @@ +package com.microsoft.recognizers.text.datetime.config; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; + +public interface IOptionsConfiguration { + DateTimeOptions getOptions(); + + boolean getDmyDateFormat(); + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateExtractorConfiguration.java new file mode 100644 index 000000000..588917bf8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateExtractorConfiguration.java @@ -0,0 +1,241 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.utilities.EnglishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.number.english.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.english.parsers.EnglishNumberParserConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class EnglishDateExtractorConfiguration extends BaseOptionsConfiguration implements IDateExtractorConfiguration { + + public static final Pattern MonthRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthRegex); + public static final Pattern DayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ImplicitDayRegex); + public static final Pattern MonthNumRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthNumRegex); + public static final Pattern YearRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.YearRegex); + public static final Pattern WeekDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekDayRegex); + public static final Pattern SingleWeekDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SingleWeekDayRegex); + public static final Pattern OnRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.OnRegex); + public static final Pattern RelaxedOnRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelaxedOnRegex); + public static final Pattern ThisRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ThisRegex); + public static final Pattern LastDateRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LastDateRegex); + public static final Pattern NextDateRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NextDateRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DateUnitRegex); + public static final Pattern SpecialDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecialDayRegex); + public static final Pattern WeekDayOfMonthRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekDayOfMonthRegex); + public static final Pattern RelativeWeekDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeWeekDayRegex); + public static final Pattern SpecialDate = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecialDate); + public static final Pattern SpecialDayWithNumRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecialDayWithNumRegex); + public static final Pattern ForTheRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ForTheRegex); + public static final Pattern WeekDayAndDayOfMonthRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekDayAndDayOfMonthRegex); + public static final Pattern RelativeMonthRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeMonthRegex); + public static final Pattern StrictRelativeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.StrictRelativeRegex); + public static final Pattern PrefixArticleRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PrefixArticleRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.InConnectorRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RangeUnitRegex); + public static final Pattern RangeConnectorSymbolRegex = RegExpUtility.getSafeRegExp(BaseDateTime.RangeConnectorSymbolRegex); + + public static final List DateRegexList = new ArrayList() { + { + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor1)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor3)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor4)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor5)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor6)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor7L)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor7S)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor8)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor9L)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractor9S)); + add(RegExpUtility.getSafeRegExp(EnglishDateTime.DateExtractorA)); + } + }; + + public static final List ImplicitDateList = new ArrayList() { + { + add(OnRegex); + add(RelaxedOnRegex); + add(SpecialDayRegex); + add(ThisRegex); + add(LastDateRegex); + add(NextDateRegex); + add(SingleWeekDayRegex); + add(WeekDayOfMonthRegex); + add(SpecialDate); + add(SpecialDayWithNumRegex); + add(RelativeWeekDayRegex); + } + }; + + public static final Pattern OfMonth = RegExpUtility.getSafeRegExp(EnglishDateTime.OfMonth); + public static final Pattern MonthEnd = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthEnd); + public static final Pattern WeekDayEnd = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekDayEnd); + public static final Pattern YearSuffix = RegExpUtility.getSafeRegExp(EnglishDateTime.YearSuffix); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LessThanRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MoreThanRegex); + + public static final ImmutableMap DayOfWeek = EnglishDateTime.DayOfWeek; + public static final ImmutableMap MonthOfYear = EnglishDateTime.MonthOfYear; + + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final List implicitDateList; + + public EnglishDateExtractorConfiguration(IOptionsConfiguration config) { + super(config.getOptions()); + integerExtractor = IntegerExtractor.getInstance(); + ordinalExtractor = OrdinalExtractor.getInstance(); + numberParser = new BaseNumberParser(new EnglishNumberParserConfiguration()); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration()); + utilityConfiguration = new EnglishDatetimeUtilityConfiguration(); + + implicitDateList = new ArrayList<>(ImplicitDateList); + if (this.getOptions().match(DateTimeOptions.CalendarMode)) { + implicitDateList.add(DayRegex); + } + } + + @Override + public Iterable getDateRegexList() { + return DateRegexList; + } + + @Override + public Iterable getImplicitDateList() { + return implicitDateList; + } + + @Override + public Pattern getOfMonth() { + return OfMonth; + } + + @Override + public Pattern getMonthEnd() { + return MonthEnd; + } + + @Override + public Pattern getWeekDayEnd() { + return WeekDayEnd; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getForTheRegex() { + return ForTheRegex; + } + + @Override + public Pattern getWeekDayAndDayOfMonthRegex() { + return WeekDayAndDayOfMonthRegex; + } + + @Override + public Pattern getRelativeMonthRegex() { + return RelativeMonthRegex; + } + + @Override + public Pattern getStrictRelativeRegex() { + return StrictRelativeRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return WeekDayRegex; + } + + @Override + public Pattern getPrefixArticleRegex() { + return PrefixArticleRegex; + } + + @Override + public Pattern getYearSuffix() { + return YearSuffix; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getRangeConnectorSymbolRegex() { + return RangeConnectorSymbolRegex; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public ImmutableMap getDayOfWeek() { + return DayOfWeek; + } + + @Override + public ImmutableMap getMonthOfYear() { + return MonthOfYear; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDatePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDatePeriodExtractorConfiguration.java new file mode 100644 index 000000000..2e2019b79 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDatePeriodExtractorConfiguration.java @@ -0,0 +1,311 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.number.english.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.number.english.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.english.parsers.EnglishNumberParserConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public class EnglishDatePeriodExtractorConfiguration extends BaseOptionsConfiguration implements IDatePeriodExtractorConfiguration { + + public static final Pattern YearRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.YearRegex); + public static final Pattern TillRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TillRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DateUnitRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeUnitRegex); + public static final Pattern FollowedDateUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.FollowedDateUnit); + public static final Pattern NumberCombinedWithDateUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.NumberCombinedWithDateUnit); + public static final Pattern PreviousPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PreviousPrefixRegex); + public static final Pattern NextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NextPrefixRegex); + public static final Pattern FutureSuffixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.FutureSuffixRegex); + public static final Pattern WeekOfRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekOfRegex); + public static final Pattern MonthOfRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthOfRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RangeUnitRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.InConnectorRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WithinNextPrefixRegex); + public static final Pattern YearPeriodRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.YearPeriodRegex); + public static final Pattern RelativeDecadeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeDecadeRegex); + public static final Pattern ComplexDatePeriodRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ComplexDatePeriodRegex); + public static final Pattern ReferenceDatePeriodRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ReferenceDatePeriodRegex); + public static final Pattern AgoRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AgoRegex); + public static final Pattern LaterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LaterRegex); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LessThanRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MoreThanRegex); + public static final Pattern CenturySuffixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.CenturySuffixRegex); + public static final Pattern IllegalYearRegex = RegExpUtility.getSafeRegExp(BaseDateTime.IllegalYearRegex); + public static final Pattern NowRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NowRegex); + + // composite regexes + public static final Pattern SimpleCasesRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SimpleCasesRegex); + public static final Pattern BetweenRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.BetweenRegex); + public static final Pattern OneWordPeriodRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.OneWordPeriodRegex); + public static final Pattern MonthWithYear = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthWithYear); + public static final Pattern MonthNumWithYear = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthNumWithYear); + public static final Pattern WeekOfMonthRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekOfMonthRegex); + public static final Pattern WeekOfYearRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekOfYearRegex); + public static final Pattern MonthFrontBetweenRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthFrontBetweenRegex); + public static final Pattern MonthFrontSimpleCasesRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MonthFrontSimpleCasesRegex); + public static final Pattern QuarterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.QuarterRegex); + public static final Pattern QuarterRegexYearFront = RegExpUtility.getSafeRegExp(EnglishDateTime.QuarterRegexYearFront); + public static final Pattern AllHalfYearRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AllHalfYearRegex); + public static final Pattern SeasonRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SeasonRegex); + public static final Pattern WhichWeekRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WhichWeekRegex); + public static final Pattern RestOfDateRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RestOfDateRegex); + public static final Pattern LaterEarlyPeriodRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LaterEarlyPeriodRegex); + public static final Pattern WeekWithWeekDayRangeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekWithWeekDayRangeRegex); + public static final Pattern YearPlusNumberRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.YearPlusNumberRegex); + public static final Pattern DecadeWithCenturyRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DecadeWithCenturyRegex); + + public static final Iterable SimpleCasesRegexes = new ArrayList() { + { + add(SimpleCasesRegex); + add(BetweenRegex); + add(OneWordPeriodRegex); + add(MonthWithYear); + add(MonthNumWithYear); + add(YearRegex); + add(WeekOfMonthRegex); + add(WeekOfYearRegex); + add(MonthFrontBetweenRegex); + add(MonthFrontSimpleCasesRegex); + add(QuarterRegex); + add(QuarterRegexYearFront); + add(AllHalfYearRegex); + add(SeasonRegex); + add(WhichWeekRegex); + add(RestOfDateRegex); + add(LaterEarlyPeriodRegex); + add(WeekWithWeekDayRangeRegex); + add(YearPlusNumberRegex); + add(DecadeWithCenturyRegex); + add(RelativeDecadeRegex); + add(ReferenceDatePeriodRegex); + } + }; + + public static final Pattern rangeConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RangeConnectorRegex); + private final String[] durationDateRestrictions = EnglishDateTime.DurationDateRestrictions.toArray(new String[0]); + + private final IDateTimeExtractor datePointExtractor; + private final IExtractor cardinalExtractor; + private final IExtractor ordinalExtractor; + private final IDateTimeExtractor durationExtractor; + private final IParser numberParser; + + public EnglishDatePeriodExtractorConfiguration(IOptionsConfiguration config) { + super(config.getOptions()); + + datePointExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + cardinalExtractor = CardinalExtractor.getInstance(); + ordinalExtractor = OrdinalExtractor.getInstance(); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration()); + numberParser = new BaseNumberParser(new EnglishNumberParserConfiguration()); + } + + @Override + public Iterable getSimpleCasesRegexes() { + return SimpleCasesRegexes; + } + + @Override + public Pattern getIllegalYearRegex() { + return IllegalYearRegex; + } + + @Override + public Pattern getYearRegex() { + return YearRegex; + } + + @Override + public Pattern getTillRegex() { + return TillRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getFollowedDateUnit() { + return FollowedDateUnit; + } + + @Override + public Pattern getNumberCombinedWithDateUnit() { + return NumberCombinedWithDateUnit; + } + + @Override + public Pattern getPastRegex() { + return PreviousPrefixRegex; + } + + @Override + public Pattern getFutureRegex() { + return NextPrefixRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return FutureSuffixRegex; + } + + @Override + public Pattern getWeekOfRegex() { + return WeekOfRegex; + } + + @Override + public Pattern getMonthOfRegex() { + return MonthOfRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getYearPeriodRegex() { + return YearPeriodRegex; + } + + @Override + public Pattern getRelativeDecadeRegex() { + return RelativeDecadeRegex; + } + + @Override + public Pattern getComplexDatePeriodRegex() { + return ComplexDatePeriodRegex; + } + + @Override + public Pattern getReferenceDatePeriodRegex() { + return ReferenceDatePeriodRegex; + } + + @Override + public Pattern getAgoRegex() { + return AgoRegex; + } + + @Override + public Pattern getLaterRegex() { + return LaterRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public Pattern getCenturySuffixRegex() { + return CenturySuffixRegex; + } + + @Override + public Pattern getNowRegex() { + return NowRegex; + } + + @Override + public IDateTimeExtractor getDatePointExtractor() { + return datePointExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public String[] getDurationDateRestrictions() { + return durationDateRestrictions; + } + + @Override + public ResultIndex getFromTokenIndex(String text) { + int index = -1; + boolean result = false; + if (text.endsWith("from")) { + result = true; + index = text.lastIndexOf("from"); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(String text) { + int index = -1; + boolean result = false; + if (text.endsWith("between")) { + result = true; + index = text.lastIndexOf("between"); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(String text) { + return RegexExtension.isExactMatch(rangeConnectorRegex, text, true); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimeAltExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimeAltExtractorConfiguration.java new file mode 100644 index 000000000..75c75d11e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimeAltExtractorConfiguration.java @@ -0,0 +1,90 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeAltExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class EnglishDateTimeAltExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimeAltExtractorConfiguration { + + private static final Pattern OrRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.OrRegex); + private static final Pattern DayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DayRegex); + private static final Pattern RangePrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RangePrefixRegex); + + public static final Pattern ThisPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ThisPrefixRegex); + public static final Pattern PreviousPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PreviousPrefixRegex); + public static final Pattern NextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NextPrefixRegex); + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AmRegex); + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PmRegex); + + public static final Iterable RelativePrefixList = new ArrayList() { + { + add(ThisPrefixRegex); + add(PreviousPrefixRegex); + add(NextPrefixRegex); + } + }; + + public static final Iterable AmPmRegexList = new ArrayList() { + { + add(AmRegex); + add(PmRegex); + } + }; + + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor datePeriodExtractor; + + public EnglishDateTimeAltExtractorConfiguration(IOptionsConfiguration config) { + super(config.getOptions()); + dateExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + datePeriodExtractor = new BaseDatePeriodExtractor(new EnglishDatePeriodExtractorConfiguration(this)); + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + @Override + public Iterable getRelativePrefixList() { + return RelativePrefixList; + } + + @Override + public Iterable getAmPmRegexList() { + return AmPmRegexList; + } + + @Override + public Pattern getOrRegex() { + return OrRegex; + } + + @Override + public Pattern getThisPrefixRegex() { + return ThisPrefixRegex; + } + + @Override + public Pattern getDayRegex() { + return DayRegex; + } + + @Override public Pattern getRangePrefixRegex() { + return RangePrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimeExtractorConfiguration.java new file mode 100644 index 000000000..317ade20d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimeExtractorConfiguration.java @@ -0,0 +1,160 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.utilities.EnglishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.Arrays; +import java.util.regex.Pattern; + +public class EnglishDateTimeExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimeExtractorConfiguration { + + public static final Pattern PrepositionRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PrepositionRegex); + public static final Pattern NowRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NowRegex); + public static final Pattern SuffixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SuffixRegex); + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeOfDayRegex); + public static final Pattern SpecificTimeOfDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecificTimeOfDayRegex); + public static final Pattern TimeOfTodayAfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeOfTodayAfterRegex); + public static final Pattern TimeOfTodayBeforeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeOfTodayBeforeRegex); + public static final Pattern SimpleTimeOfTodayAfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SimpleTimeOfTodayAfterRegex); + public static final Pattern SimpleTimeOfTodayBeforeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SimpleTimeOfTodayBeforeRegex); + public static final Pattern SpecificEndOfRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecificEndOfRegex); + public static final Pattern UnspecificEndOfRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.UnspecificEndOfRegex); + public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeUnitRegex); + public static final Pattern ConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ConnectorRegex); + public static final Pattern NumberAsTimeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NumberAsTimeRegex); + public static final Pattern DateNumberConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DateNumberConnectorRegex); + public static final Pattern SuffixAfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SuffixAfterRegex); + + public IExtractor integerExtractor; + public IDateTimeExtractor datePointExtractor; + public IDateTimeExtractor timePointExtractor; + public IDateTimeExtractor durationExtractor; + public IDateTimeUtilityConfiguration utilityConfiguration; + + public EnglishDateTimeExtractorConfiguration(DateTimeOptions options) { + + super(options); + + integerExtractor = IntegerExtractor.getInstance(); + datePointExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + timePointExtractor = new BaseTimeExtractor(new EnglishTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration(options)); + + utilityConfiguration = new EnglishDatetimeUtilityConfiguration(); + } + + public EnglishDateTimeExtractorConfiguration() { + this(DateTimeOptions.None); + } + + @Override + public Pattern getNowRegex() { + return NowRegex; + } + + @Override + public Pattern getSuffixRegex() { + return SuffixRegex; + } + + @Override + public Pattern getTimeOfTodayAfterRegex() { + return TimeOfTodayAfterRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayAfterRegex() { + return SimpleTimeOfTodayAfterRegex; + } + + @Override + public Pattern getTimeOfTodayBeforeRegex() { + return TimeOfTodayBeforeRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayBeforeRegex() { + return SimpleTimeOfTodayBeforeRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return TimeOfDayRegex; + } + + @Override + public Pattern getSpecificEndOfRegex() { + return SpecificEndOfRegex; + } + + @Override + public Pattern getUnspecificEndOfRegex() { + return UnspecificEndOfRegex; + } + + @Override + public Pattern getUnitRegex() { + return UnitRegex; + } + + @Override + public Pattern getNumberAsTimeRegex() { + return NumberAsTimeRegex; + } + + @Override + public Pattern getDateNumberConnectorRegex() { + return DateNumberConnectorRegex; + } + + @Override + public Pattern getSuffixAfterRegex() { + return SuffixAfterRegex; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getDatePointExtractor() { + return datePointExtractor; + } + + @Override + public IDateTimeExtractor getTimePointExtractor() { + return timePointExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + public boolean isConnector(String text) { + + text = text.trim(); + + boolean isPreposition = Arrays.stream(RegExpUtility.getMatches(PrepositionRegex, text)).findFirst().isPresent(); + boolean isConnector = Arrays.stream(RegExpUtility.getMatches(ConnectorRegex, text)).findFirst().isPresent(); + return (StringUtility.isNullOrEmpty(text) || isPreposition || isConnector); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..6ddc07c71 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDateTimePeriodExtractorConfiguration.java @@ -0,0 +1,281 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.number.english.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public class EnglishDateTimePeriodExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimePeriodExtractorConfiguration { + + public static final Iterable SimpleCases = new ArrayList() { + { + add(EnglishTimePeriodExtractorConfiguration.PureNumFromTo); + add(EnglishTimePeriodExtractorConfiguration.PureNumBetweenAnd); + } + }; + + public static final Pattern PeriodTimeOfDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PeriodTimeOfDayRegex); + public static final Pattern PeriodSpecificTimeOfDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PeriodSpecificTimeOfDayRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeUnitRegex); + public static final Pattern TimeFollowedUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeFollowedUnit); + public static final Pattern TimeNumberCombinedWithUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeNumberCombinedWithUnit); + public static final Pattern PeriodTimeOfDayWithDateRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PeriodTimeOfDayWithDateRegex); + public static final Pattern RelativeTimeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeTimeUnitRegex); + public static final Pattern RestOfDateTimeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RestOfDateTimeRegex); + public static final Pattern GeneralEndingRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.GeneralEndingRegex); + public static final Pattern MiddlePauseRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MiddlePauseRegex); + public static final Pattern AmDescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AmDescRegex); + public static final Pattern PmDescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PmDescRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WithinNextPrefixRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DateUnitRegex); + public static final Pattern PrefixDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PrefixDayRegex); + public static final Pattern SuffixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SuffixRegex); + public static final Pattern BeforeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.BeforeRegex); + public static final Pattern AfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AfterRegex); + + private final String tokenBeforeDate; + + private final IExtractor cardinalExtractor; + private final IDateTimeExtractor singleDateExtractor; + private final IDateTimeExtractor singleTimeExtractor; + private final IDateTimeExtractor singleDateTimeExtractor; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor timeZoneExtractor; + + private final Pattern weekDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WeekDayRegex); + private final Pattern rangeConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RangeConnectorRegex); + + public EnglishDateTimePeriodExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public EnglishDateTimePeriodExtractorConfiguration(DateTimeOptions options) { + + super(options); + + //TODO add english implementations + tokenBeforeDate = EnglishDateTime.TokenBeforeDate; + + cardinalExtractor = CardinalExtractor.getInstance(); + singleDateExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + singleTimeExtractor = new BaseTimeExtractor(new EnglishTimeExtractorConfiguration(options)); + singleDateTimeExtractor = new BaseDateTimeExtractor(new EnglishDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration(options)); + timePeriodExtractor = new BaseTimePeriodExtractor(new EnglishTimePeriodExtractorConfiguration(options)); + timeZoneExtractor = new BaseTimeZoneExtractor(new EnglishTimeZoneExtractorConfiguration(options)); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public Iterable getSimpleCasesRegex() { + return SimpleCases; + } + + @Override + public Pattern getPrepositionRegex() { + return EnglishTimePeriodExtractorConfiguration.PrepositionRegex; + } + + @Override + public Pattern getTillRegex() { + return EnglishTimePeriodExtractorConfiguration.TillRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return PeriodSpecificTimeOfDayRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return PeriodTimeOfDayRegex; + } + + @Override + public Pattern getFollowedUnit() { + return TimeFollowedUnit; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return TimeNumberCombinedWithUnit; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return EnglishDatePeriodExtractorConfiguration.PreviousPrefixRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return EnglishDatePeriodExtractorConfiguration.NextPrefixRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return EnglishDatePeriodExtractorConfiguration.FutureSuffixRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return weekDayRegex; + } + + @Override + public Pattern getPeriodTimeOfDayWithDateRegex() { + return PeriodTimeOfDayWithDateRegex; + } + + @Override + public Pattern getRelativeTimeUnitRegex() { + return RelativeTimeUnitRegex; + } + + @Override + public Pattern getRestOfDateTimeRegex() { + return RestOfDateTimeRegex; + } + + @Override + public Pattern getGeneralEndingRegex() { + return GeneralEndingRegex; + } + + @Override + public Pattern getMiddlePauseRegex() { + return MiddlePauseRegex; + } + + @Override + public Pattern getAmDescRegex() { + return AmDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return PmDescRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getPrefixDayRegex() { + return PrefixDayRegex; + } + + @Override + public Pattern getSuffixRegex() { + return SuffixRegex; + } + + @Override + public Pattern getBeforeRegex() { + return BeforeRegex; + } + + @Override + public Pattern getAfterRegex() { + return AfterRegex; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IDateTimeExtractor getSingleDateExtractor() { + return singleDateExtractor; + } + + @Override + public IDateTimeExtractor getSingleTimeExtractor() { + return singleTimeExtractor; + } + + @Override + public IDateTimeExtractor getSingleDateTimeExtractor() { + return singleDateTimeExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + // TODO: these three methods are the same in DatePeriod, should be abstracted + @Override + public ResultIndex getFromTokenIndex(String text) { + int index = -1; + boolean result = false; + if (text.endsWith("from")) { + result = true; + index = text.lastIndexOf("from"); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(String text) { + int index = -1; + boolean result = false; + if (text.endsWith("between")) { + result = true; + index = text.lastIndexOf("between"); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(String text) { + return RegexExtension.isExactMatch(rangeConnectorRegex, text, true); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDurationExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDurationExtractorConfiguration.java new file mode 100644 index 000000000..773a600ef --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishDurationExtractorConfiguration.java @@ -0,0 +1,138 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.IDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.number.english.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class EnglishDurationExtractorConfiguration extends BaseOptionsConfiguration implements IDurationExtractorConfiguration { + + public static final Pattern DurationUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DurationUnitRegex); + public static final Pattern SuffixAndRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SuffixAndRegex); + public static final Pattern DurationFollowedUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.DurationFollowedUnit); + public static final Pattern NumberCombinedWithDurationUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.NumberCombinedWithDurationUnit); + public static final Pattern AnUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AnUnitRegex); + public static final Pattern DuringRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DuringRegex); + public static final Pattern AllRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AllRegex); + public static final Pattern HalfRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.HalfRegex); + public static final Pattern ConjunctionRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ConjunctionRegex); + public static final Pattern InexactNumberRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.InexactNumberRegex); + public static final Pattern InexactNumberUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.InexactNumberUnitRegex); + public static final Pattern RelativeDurationUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeDurationUnitRegex); + public static final Pattern DurationConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DurationConnectorRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MoreThanRegex); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LessThanRegex); + + private final IExtractor cardinalExtractor; + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + + public EnglishDurationExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public EnglishDurationExtractorConfiguration(DateTimeOptions options) { + + super(options); + + cardinalExtractor = CardinalExtractor.getInstance(); + unitMap = EnglishDateTime.UnitMap; + unitValueMap = EnglishDateTime.UnitValueMap; + } + + @Override + public Pattern getFollowedUnit() { + return DurationFollowedUnit; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return NumberCombinedWithDurationUnit; + } + + @Override + public Pattern getAnUnitRegex() { + return AnUnitRegex; + } + + @Override + public Pattern getDuringRegex() { + return DuringRegex; + } + + @Override + public Pattern getAllRegex() { + return AllRegex; + } + + @Override + public Pattern getHalfRegex() { + return HalfRegex; + } + + @Override + public Pattern getSuffixAndRegex() { + return SuffixAndRegex; + } + + @Override + public Pattern getConjunctionRegex() { + return ConjunctionRegex; + } + + @Override + public Pattern getInexactNumberRegex() { + return InexactNumberRegex; + } + + @Override + public Pattern getInexactNumberUnitRegex() { + return InexactNumberUnitRegex; + } + + @Override + public Pattern getRelativeDurationUnitRegex() { + return RelativeDurationUnitRegex; + } + + @Override + public Pattern getDurationUnitRegex() { + return DurationUnitRegex; + } + + @Override + public Pattern getDurationConnectorRegex() { + return DurationConnectorRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishHolidayExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishHolidayExtractorConfiguration.java new file mode 100644 index 000000000..7843fda3a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishHolidayExtractorConfiguration.java @@ -0,0 +1,31 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.IHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class EnglishHolidayExtractorConfiguration extends BaseOptionsConfiguration implements IHolidayExtractorConfiguration { + + public static final Pattern YearPattern = RegExpUtility.getSafeRegExp(EnglishDateTime.YearRegex); + + public static final Pattern H = RegExpUtility.getSafeRegExp(EnglishDateTime.HolidayRegex); + + public static final Iterable HolidayRegexList = new ArrayList() { + { + add(H); + } + }; + + public EnglishHolidayExtractorConfiguration() { + super(DateTimeOptions.None); + } + + public Iterable getHolidayRegexes() { + return HolidayRegexList; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishMergedExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishMergedExtractorConfiguration.java new file mode 100644 index 000000000..dd083d202 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishMergedExtractorConfiguration.java @@ -0,0 +1,220 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeAltExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseHolidayExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseSetExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeListExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.javatuples.Pair; + +import org.javatuples.Pair; + +public class EnglishMergedExtractorConfiguration extends BaseOptionsConfiguration implements IMergedExtractorConfiguration { + + public static final Pattern AfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AfterRegex); + public static final Pattern SinceRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SinceRegex); + public static final Pattern AroundRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AroundRegex); + public static final Pattern BeforeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.BeforeRegex); + public static final Pattern FromToRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.FromToRegex); + public static final Pattern SuffixAfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SuffixAfterRegex); + public static final Pattern NumberEndingPattern = RegExpUtility.getSafeRegExp(EnglishDateTime.NumberEndingPattern); + public static final Pattern PrepositionSuffixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PrepositionSuffixRegex); + public static final Pattern AmbiguousRangeModifierPrefix = RegExpUtility.getSafeRegExp(EnglishDateTime.AmbiguousRangeModifierPrefix); + public static final Pattern SingleAmbiguousMonthRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SingleAmbiguousMonthRegex); + public static final Pattern UnspecificDatePeriodRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.UnspecificDatePeriodRegex); + private final Iterable> ambiguityFiltersDict; + + public static final StringMatcher SuperfluousWordMatcher = new StringMatcher(); + private static final Iterable filterWordRegexList = new ArrayList() { + { + // one on one + add(RegExpUtility.getSafeRegExp(EnglishDateTime.OneOnOneRegex)); + + // (the)? (day|week|month|year) + add(RegExpUtility.getSafeRegExp(EnglishDateTime.SingleAmbiguousTermsRegex)); + } + }; + + public final Iterable getFilterWordRegexList() { + return filterWordRegexList; + } + + public final StringMatcher getSuperfluousWordMatcher() { + return SuperfluousWordMatcher; + } + + private IDateTimeExtractor setExtractor; + + public final IDateTimeExtractor getSetExtractor() { + return setExtractor; + } + + private IExtractor integerExtractor; + + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + private IDateExtractor dateExtractor; + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + private IDateTimeExtractor timeExtractor; + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + private IDateTimeExtractor holidayExtractor; + + public final IDateTimeExtractor getHolidayExtractor() { + return holidayExtractor; + } + + private IDateTimeExtractor dateTimeExtractor; + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor datePeriodExtractor; + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + private IDateTimeExtractor timePeriodExtractor; + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + private IDateTimeZoneExtractor timeZoneExtractor; + + public final IDateTimeZoneExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + private IDateTimeListExtractor dateTimeAltExtractor; + + public final IDateTimeListExtractor getDateTimeAltExtractor() { + return dateTimeAltExtractor; + } + + private IDateTimeExtractor dateTimePeriodExtractor; + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + public EnglishMergedExtractorConfiguration(DateTimeOptions options) { + super(options); + + setExtractor = new BaseSetExtractor(new EnglishSetExtractorConfiguration(options)); + dateExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + timeExtractor = new BaseTimeExtractor(new EnglishTimeExtractorConfiguration(options)); + holidayExtractor = new BaseHolidayExtractor(new EnglishHolidayExtractorConfiguration()); + datePeriodExtractor = new BaseDatePeriodExtractor(new EnglishDatePeriodExtractorConfiguration(this)); + dateTimeExtractor = new BaseDateTimeExtractor(new EnglishDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration(options)); + timeZoneExtractor = new BaseTimeZoneExtractor(new EnglishTimeZoneExtractorConfiguration(options)); + dateTimeAltExtractor = new BaseDateTimeAltExtractor(new EnglishDateTimeAltExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new EnglishTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor(new EnglishDateTimePeriodExtractorConfiguration(options)); + integerExtractor = IntegerExtractor.getInstance(); + + ambiguityFiltersDict = EnglishDateTime.AmbiguityFiltersDict.entrySet().stream().map(pair -> { + Pattern key = RegExpUtility.getSafeRegExp(pair.getKey()); + Pattern val = RegExpUtility.getSafeRegExp(pair.getValue()); + return new Pair(key, val); + }).collect(Collectors.toList()); + + if (!this.getOptions().match(DateTimeOptions.EnablePreview)) { + getSuperfluousWordMatcher().init(EnglishDateTime.SuperfluousWordList); + } + } + + public final Pattern getAfterRegex() { + return AfterRegex; + } + + public final Pattern getSinceRegex() { + return SinceRegex; + } + + + public final Pattern getAroundRegex() { + return AroundRegex; + } + + public final Pattern getBeforeRegex() { + return BeforeRegex; + } + + public final Pattern getFromToRegex() { + return FromToRegex; + } + + public final Pattern getSuffixAfterRegex() { + return SuffixAfterRegex; + } + + public final Pattern getNumberEndingPattern() { + return NumberEndingPattern; + } + + public final Pattern getPrepositionSuffixRegex() { + return PrepositionSuffixRegex; + } + + public final Pattern getAmbiguousRangeModifierPrefix() { + return AmbiguousRangeModifierPrefix; + } + + public final Pattern getPotentialAmbiguousRangeRegex() { + return FromToRegex; + } + + public final Pattern getSingleAmbiguousMonthRegex() { + return SingleAmbiguousMonthRegex; + } + + public final Pattern getUnspecificDatePeriodRegex() { + return UnspecificDatePeriodRegex; + } + + public final Iterable> getAmbiguityFiltersDict() { + return ambiguityFiltersDict; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishSetExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishSetExtractorConfiguration.java new file mode 100644 index 000000000..6879dc559 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishSetExtractorConfiguration.java @@ -0,0 +1,120 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ISetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class EnglishSetExtractorConfiguration extends BaseOptionsConfiguration implements ISetExtractorConfiguration { + + public static final Pattern SetLastRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SetLastRegex); + public static final Pattern EachDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.EachDayRegex); + public static final Pattern SetEachRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SetEachRegex); + public static final Pattern PeriodicRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PeriodicRegex); + public static final Pattern EachUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.EachUnitRegex); + public static final Pattern SetUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DurationUnitRegex); + public static final Pattern EachPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.EachPrefixRegex); + public static final Pattern SetWeekDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SetWeekDayRegex); + + public EnglishSetExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public EnglishSetExtractorConfiguration(DateTimeOptions options) { + super(options); + + timeExtractor = new BaseTimeExtractor(new EnglishTimeExtractorConfiguration(options)); + dateExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration()); + dateTimeExtractor = new BaseDateTimeExtractor(new EnglishDateTimeExtractorConfiguration(options)); + datePeriodExtractor = new BaseDatePeriodExtractor(new EnglishDatePeriodExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new EnglishTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor(new EnglishDateTimePeriodExtractorConfiguration(options)); + } + + private IDateTimeExtractor timeExtractor; + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + private IDateExtractor dateExtractor; + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor dateTimeExtractor; + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + private IDateTimeExtractor datePeriodExtractor; + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + private IDateTimeExtractor timePeriodExtractor; + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + private IDateTimeExtractor dateTimePeriodExtractor; + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + public final Pattern getLastRegex() { + return SetLastRegex; + } + + public final Pattern getBeforeEachDayRegex() { + return null; + } + + public final Pattern getEachDayRegex() { + return EachDayRegex; + } + + public final Pattern getSetEachRegex() { + return SetEachRegex; + } + + public final Pattern getPeriodicRegex() { + return PeriodicRegex; + } + + public final Pattern getEachUnitRegex() { + return EachUnitRegex; + } + + public final Pattern getSetWeekDayRegex() { + return SetWeekDayRegex; + } + + public final Pattern getEachPrefixRegex() { + return EachPrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimeExtractorConfiguration.java new file mode 100644 index 000000000..d4d1c4a61 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimeExtractorConfiguration.java @@ -0,0 +1,140 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class EnglishTimeExtractorConfiguration extends BaseOptionsConfiguration implements ITimeExtractorConfiguration { + + // part 1: smallest component + // -------------------------------------- + public static final Pattern DescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DescRegex); + public static final Pattern HourNumRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.HourNumRegex); + public static final Pattern MinuteNumRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MinuteNumRegex); + + // part 2: middle level component + // -------------------------------------- + // handle "... o'clock" + public static final Pattern OclockRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.OclockRegex); + + // handle "... afternoon" + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PmRegex); + + // handle "... in the morning" + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AmRegex); + + // handle "half past ..." "a quarter to ..." + // rename 'min' group to 'deltamin' + public static final Pattern LessThanOneHour = RegExpUtility.getSafeRegExp(EnglishDateTime.LessThanOneHour); + + // handle "six thirty", "six twenty one" + public static final Pattern BasicTime = RegExpUtility.getSafeRegExp(EnglishDateTime.BasicTime); + public static final Pattern TimePrefix = RegExpUtility.getSafeRegExp(EnglishDateTime.TimePrefix); + public static final Pattern TimeSuffix = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeSuffix); + public static final Pattern WrittenTimeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WrittenTimeRegex); + + // handle special time such as 'at midnight', 'midnight', 'midday' + public static final Pattern MiddayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MiddayRegex); + public static final Pattern MidTimeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MidTimeRegex); + public static final Pattern MidnightRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MidnightRegex); + public static final Pattern MidmorningRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MidmorningRegex); + public static final Pattern MidafternoonRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MidafternoonRegex); + + // part 3: regex for time + // -------------------------------------- + // handle "at four" "at 3" + public static final Pattern AtRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AtRegex); + public static final Pattern IshRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.IshRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeUnitRegex); + public static final Pattern ConnectNumRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ConnectNumRegex); + public static final Pattern TimeBeforeAfterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeBeforeAfterRegex); + + public static final Iterable TimeRegexList = new ArrayList() { + { + // (three min past)? seven|7|(senven thirty) pm + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex1)); + + // (three min past)? 3:00(:00)? (pm)? + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex2)); + + // (three min past)? 3.00 (pm) + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex3)); + + // (three min past) (five thirty|seven|7|7:00(:00)?) (pm)? (in the night) + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex4)); + + // (three min past) (five thirty|seven|7|7:00(:00)?) (pm)? + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex5)); + + // (five thirty|seven|7|7:00(:00)?) (pm)? (in the night) + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex6)); + + // (in the night) at (five thirty|seven|7|7:00(:00)?) (pm)? + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex7)); + + // (in the night) (five thirty|seven|7|7:00(:00)?) (pm)? + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex8)); + + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex9)); + + // (three min past)? 3h00 (pm)? + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex10)); + + // at 2.30, "at" prefix is required here + // 3.30pm, "am/pm" suffix is required here + add(RegExpUtility.getSafeRegExp(EnglishDateTime.TimeRegex11)); + + // 340pm + add(ConnectNumRegex); + } + }; + + public final Iterable getTimeRegexList() { + return TimeRegexList; + } + + public final Pattern getAtRegex() { + return AtRegex; + } + + public final Pattern getIshRegex() { + return IshRegex; + } + + public final Pattern getTimeBeforeAfterRegex() { + return TimeBeforeAfterRegex; + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor timeZoneExtractor; + + public final IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + + public EnglishTimeExtractorConfiguration() { + this(DateTimeOptions.None); + } + + //C# TO JAVA CONVERTER NOTE: Java does not support optional parameters. Overloaded method(s) are created above: + //ORIGINAL LINE: public EnglishTimeExtractorConfiguration(DateTimeOptions options = DateTimeOptions.None) + public EnglishTimeExtractorConfiguration(DateTimeOptions options) { + super(options); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration()); + timeZoneExtractor = new BaseTimeZoneExtractor(new EnglishTimeZoneExtractorConfiguration(options)); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..bdf1e1e50 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimePeriodExtractorConfiguration.java @@ -0,0 +1,132 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.utilities.EnglishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class EnglishTimePeriodExtractorConfiguration extends BaseOptionsConfiguration implements ITimePeriodExtractorConfiguration { + + private String tokenBeforeDate; + + public final String getTokenBeforeDate() { + return tokenBeforeDate; + } + + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AmRegex); + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PmRegex); + public static final Pattern HourRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.HourRegex); + public static final Pattern TillRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TillRegex); + public static final Pattern PeriodDescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DescRegex); + public static final Pattern PureNumFromTo = RegExpUtility.getSafeRegExp(EnglishDateTime.PureNumFromTo); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeUnitRegex); + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeOfDayRegex); + public static final Pattern PrepositionRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PrepositionRegex); + public static final Pattern TimeFollowedUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeFollowedUnit); + public static final Pattern PureNumBetweenAnd = RegExpUtility.getSafeRegExp(EnglishDateTime.PureNumBetweenAnd); + public static final Pattern GeneralEndingRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.GeneralEndingRegex); + public static final Pattern PeriodHourNumRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PeriodHourNumRegex); + public static final Pattern SpecificTimeFromTo = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecificTimeFromTo); + public static final Pattern SpecificTimeBetweenAnd = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecificTimeBetweenAnd); + public static final Pattern SpecificTimeOfDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.SpecificTimeOfDayRegex); + public static final Pattern TimeNumberCombinedWithUnit = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeNumberCombinedWithUnit); + + public EnglishTimePeriodExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public EnglishTimePeriodExtractorConfiguration(DateTimeOptions options) { + + super(options); + + tokenBeforeDate = EnglishDateTime.TokenBeforeDate; + singleTimeExtractor = new BaseTimeExtractor(new EnglishTimeExtractorConfiguration(options)); + utilityConfiguration = new EnglishDatetimeUtilityConfiguration(); + integerExtractor = IntegerExtractor.getInstance(); + timeZoneExtractor = new BaseTimeZoneExtractor(new EnglishTimeZoneExtractorConfiguration(options)); + } + + private IDateTimeUtilityConfiguration utilityConfiguration; + + public final IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + private IDateTimeExtractor singleTimeExtractor; + + public final IDateTimeExtractor getSingleTimeExtractor() { + return singleTimeExtractor; + } + + private IExtractor integerExtractor; + + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + private final IDateTimeExtractor timeZoneExtractor; + + public IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + + public Iterable getSimpleCasesRegex() { + return getSimpleCasesRegex; + } + + public final Iterable getSimpleCasesRegex = new ArrayList() { + { + add(PureNumFromTo); + add(PureNumBetweenAnd); + add(SpecificTimeFromTo); + add(SpecificTimeBetweenAnd); + } + }; + + public final Pattern getTillRegex() { + return TillRegex; + } + + public final Pattern getTimeOfDayRegex() { + return TimeOfDayRegex; + } + + public final Pattern getGeneralEndingRegex() { + return GeneralEndingRegex; + } + + public final ResultIndex getFromTokenIndex(String input) { + ResultIndex result = new ResultIndex(false, -1); + if (input.endsWith("from")) { + result = new ResultIndex(true, input.lastIndexOf("from")); + } + + return result; + } + + public final ResultIndex getBetweenTokenIndex(String input) { + ResultIndex result = new ResultIndex(false, -1); + if (input.endsWith("between")) { + result = new ResultIndex(true, input.lastIndexOf("between")); + } + + return result; + } + + public final boolean hasConnectorToken(String input) { + return input.equals("and"); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimeZoneExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimeZoneExtractorConfiguration.java new file mode 100644 index 000000000..71cbb7aff --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/extractors/EnglishTimeZoneExtractorConfiguration.java @@ -0,0 +1,93 @@ +package com.microsoft.recognizers.text.datetime.english.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeZoneExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishTimeZone; +import com.microsoft.recognizers.text.matcher.MatchStrategy; +import com.microsoft.recognizers.text.matcher.NumberWithUnitTokenizer; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import com.microsoft.recognizers.text.utilities.QueryProcessor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class EnglishTimeZoneExtractorConfiguration extends BaseOptionsConfiguration implements ITimeZoneExtractorConfiguration { + + // These regexes do need to be case insensitive for them to work correctly + public static final Pattern DirectUtcRegex = RegExpUtility.getSafeRegExp(EnglishTimeZone.DirectUtcRegex, Pattern.CASE_INSENSITIVE); + public static final List AbbreviationsList = EnglishTimeZone.AbbreviationsList; + public static final List FullNameList = EnglishTimeZone.FullNameList; + public static final Pattern LocationTimeSuffixRegex = RegExpUtility.getSafeRegExp(EnglishTimeZone.LocationTimeSuffixRegex, Pattern.CASE_INSENSITIVE); + public static final StringMatcher LocationMatcher = new StringMatcher(); + public static final StringMatcher TimeZoneMatcher = buildMatcherFromLists(AbbreviationsList, FullNameList); + + public static final List AmbiguousTimezoneList = EnglishTimeZone.AmbiguousTimezoneList; + + public EnglishTimeZoneExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public EnglishTimeZoneExtractorConfiguration(DateTimeOptions options) { + + super(options); + + if (options.match(DateTimeOptions.EnablePreview)) { + LocationMatcher.init( + EnglishTimeZone.MajorLocations.stream() + .map(o -> QueryProcessor.removeDiacritics(o.toLowerCase())) + .collect(Collectors.toCollection(ArrayList::new))); + } + } + + protected static StringMatcher buildMatcherFromLists(List...collections) { + StringMatcher matcher = new StringMatcher(MatchStrategy.TrieTree, new NumberWithUnitTokenizer()); + List matcherList = new ArrayList(); + + for (List collection : collections) { + for (String item : collection) { + matcherList.add(item.toLowerCase()); + } + } + + matcherList.stream().forEach( + item -> { + if (!matcherList.contains(item)) { + matcherList.add(item); + } + } + ); + + matcher.init(matcherList); + + return matcher; + } + + @Override + public Pattern getDirectUtcRegex() { + return DirectUtcRegex; + } + + @Override + public Pattern getLocationTimeSuffixRegex() { + return LocationTimeSuffixRegex; + } + + @Override + public StringMatcher getLocationMatcher() { + return LocationMatcher; + } + + @Override + public StringMatcher getTimeZoneMatcher() { + return TimeZoneMatcher; + } + + @Override + public List getAmbiguousTimezoneList() { + return AmbiguousTimezoneList; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishCommonDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishCommonDateTimeParserConfiguration.java new file mode 100644 index 000000000..c4a219fec --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishCommonDateTimeParserConfiguration.java @@ -0,0 +1,290 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.utilities.EnglishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDatePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimeAltParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDurationParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.BaseDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.english.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.number.english.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.english.parsers.EnglishNumberParserConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; + +public class EnglishCommonDateTimeParserConfiguration extends BaseDateParserConfiguration implements ICommonDateTimeParserConfiguration { + + private final IDateTimeUtilityConfiguration utilityConfiguration; + + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + private final ImmutableMap seasonMap; + private final ImmutableMap specialYearPrefixesMap; + private final ImmutableMap cardinalMap; + private final ImmutableMap dayOfWeekMap; + private final ImmutableMap dayOfMonth; + private final ImmutableMap monthOfYear; + private final ImmutableMap numbers; + private final ImmutableMap doubleNumbers; + private final ImmutableMap writtenDecades; + private final ImmutableMap specialDecadeCases; + + private final IExtractor cardinalExtractor; + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IParser numberParser; + + private final IDateTimeExtractor durationExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor datePeriodExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor dateTimePeriodExtractor; + + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser durationParser; + private final IDateTimeParser datePeriodParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser dateTimePeriodParser; + private final IDateTimeParser dateTimeAltParser; + private final IDateTimeParser timeZoneParser; + + public EnglishCommonDateTimeParserConfiguration(DateTimeOptions options) { + + super(options); + + utilityConfiguration = new EnglishDatetimeUtilityConfiguration(); + + unitMap = EnglishDateTime.UnitMap; + unitValueMap = EnglishDateTime.UnitValueMap; + seasonMap = EnglishDateTime.SeasonMap; + specialYearPrefixesMap = EnglishDateTime.SpecialYearPrefixesMap; + cardinalMap = EnglishDateTime.CardinalMap; + dayOfWeekMap = EnglishDateTime.DayOfWeek; + dayOfMonth = ImmutableMap.builder().putAll(super.getDayOfMonth()).putAll(EnglishDateTime.DayOfMonth).build(); + monthOfYear = EnglishDateTime.MonthOfYear; + numbers = EnglishDateTime.Numbers; + doubleNumbers = EnglishDateTime.DoubleNumbers; + writtenDecades = EnglishDateTime.WrittenDecades; + specialDecadeCases = EnglishDateTime.SpecialDecadeCases; + + cardinalExtractor = CardinalExtractor.getInstance(); + integerExtractor = IntegerExtractor.getInstance(); + ordinalExtractor = OrdinalExtractor.getInstance(); + numberParser = new BaseNumberParser(new EnglishNumberParserConfiguration()); + + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration()); + dateExtractor = new BaseDateExtractor(new EnglishDateExtractorConfiguration(this)); + timeExtractor = new BaseTimeExtractor(new EnglishTimeExtractorConfiguration(options)); + dateTimeExtractor = new BaseDateTimeExtractor(new EnglishDateTimeExtractorConfiguration(options)); + datePeriodExtractor = new BaseDatePeriodExtractor(new EnglishDatePeriodExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new EnglishTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor(new EnglishDateTimePeriodExtractorConfiguration(options)); + + timeZoneParser = new BaseTimeZoneParser(); + durationParser = new BaseDurationParser(new EnglishDurationParserConfiguration(this)); + dateParser = new BaseDateParser(new EnglishDateParserConfiguration(this)); + timeParser = new TimeParser(new EnglishTimeParserConfiguration(this)); + dateTimeParser = new BaseDateTimeParser(new EnglishDateTimeParserConfiguration(this)); + datePeriodParser = new BaseDatePeriodParser(new EnglishDatePeriodParserConfiguration(this)); + timePeriodParser = new BaseTimePeriodParser(new EnglishTimePeriodParserConfiguration(this)); + dateTimePeriodParser = new BaseDateTimePeriodParser(new EnglishDateTimePeriodParserConfiguration(this)); + dateTimeAltParser = new BaseDateTimeAltParser(new EnglishDateTimeAltParserConfiguration(this)); + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } + + @Override + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + @Override + public IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + @Override + public IDateTimeParser getDateTimeAltParser() { + return dateTimeAltParser; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } + + @Override + public ImmutableMap getSeasonMap() { + return seasonMap; + } + + @Override + public ImmutableMap getSpecialYearPrefixesMap() { + return specialYearPrefixesMap; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public ImmutableMap getDayOfWeek() { + return dayOfWeekMap; + } + + @Override + public ImmutableMap getDoubleNumbers() { + return doubleNumbers; + } + + @Override + public ImmutableMap getWrittenDecades() { + return writtenDecades; + } + + @Override + public ImmutableMap getSpecialDecadeCases() { + return specialDecadeCases; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateParserConfiguration.java new file mode 100644 index 000000000..472b5861a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateParserConfiguration.java @@ -0,0 +1,351 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class EnglishDateParserConfiguration extends BaseOptionsConfiguration implements IDateParserConfiguration { + + public EnglishDateParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + dateTokenPrefix = EnglishDateTime.DateTokenPrefix; + + integerExtractor = config.getIntegerExtractor(); + ordinalExtractor = config.getOrdinalExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + numberParser = config.getNumberParser(); + + durationExtractor = config.getDurationExtractor(); + dateExtractor = config.getDateExtractor(); + durationParser = config.getDurationParser(); + + dateRegexes = Collections.unmodifiableList(EnglishDateExtractorConfiguration.DateRegexList); + onRegex = EnglishDateExtractorConfiguration.OnRegex; + specialDayRegex = EnglishDateExtractorConfiguration.SpecialDayRegex; + specialDayWithNumRegex = EnglishDateExtractorConfiguration.SpecialDayWithNumRegex; + nextRegex = EnglishDateExtractorConfiguration.NextDateRegex; + thisRegex = EnglishDateExtractorConfiguration.ThisRegex; + lastRegex = EnglishDateExtractorConfiguration.LastDateRegex; + unitRegex = EnglishDateExtractorConfiguration.DateUnitRegex; + weekDayRegex = EnglishDateExtractorConfiguration.WeekDayRegex; + monthRegex = EnglishDateExtractorConfiguration.MonthRegex; + weekDayOfMonthRegex = EnglishDateExtractorConfiguration.WeekDayOfMonthRegex; + forTheRegex = EnglishDateExtractorConfiguration.ForTheRegex; + weekDayAndDayOfMonthRegex = EnglishDateExtractorConfiguration.WeekDayAndDayOfMonthRegex; + relativeMonthRegex = EnglishDateExtractorConfiguration.RelativeMonthRegex; + strictRelativeRegex = EnglishDateExtractorConfiguration.StrictRelativeRegex; + relativeWeekDayRegex = EnglishDateExtractorConfiguration.RelativeWeekDayRegex; + + yearSuffix = EnglishDateExtractorConfiguration.YearSuffix; + unitMap = config.getUnitMap(); + dayOfMonth = config.getDayOfMonth(); + dayOfWeek = config.getDayOfWeek(); + monthOfYear = config.getMonthOfYear(); + cardinalMap = config.getCardinalMap(); + utilityConfiguration = config.getUtilityConfiguration(); + + relativeDayRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeDayRegex); + nextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NextPrefixRegex); + previousPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PreviousPrefixRegex); + sameDayTerms = Collections.unmodifiableList(EnglishDateTime.SameDayTerms); + plusOneDayTerms = Collections.unmodifiableList(EnglishDateTime.PlusOneDayTerms); + plusTwoDayTerms = Collections.unmodifiableList(EnglishDateTime.PlusTwoDayTerms); + minusOneDayTerms = Collections.unmodifiableList(EnglishDateTime.MinusOneDayTerms); + minusTwoDayTerms = Collections.unmodifiableList(EnglishDateTime.MinusTwoDayTerms); + + } + + private final String dateTokenPrefix; + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IExtractor cardinalExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeParser durationParser; + private final Iterable dateRegexes; + + private final Pattern onRegex; + private final Pattern specialDayRegex; + private final Pattern specialDayWithNumRegex; + private final Pattern nextRegex; + private final Pattern thisRegex; + private final Pattern lastRegex; + private final Pattern unitRegex; + private final Pattern weekDayRegex; + private final Pattern monthRegex; + private final Pattern weekDayOfMonthRegex; + private final Pattern forTheRegex; + private final Pattern weekDayAndDayOfMonthRegex; + private final Pattern relativeMonthRegex; + private final Pattern strictRelativeRegex; + private final Pattern yearSuffix; + private final Pattern relativeWeekDayRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap dayOfMonth; + private final ImmutableMap dayOfWeek; + private final ImmutableMap monthOfYear; + private final ImmutableMap cardinalMap; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + private final List sameDayTerms; + private final List plusOneDayTerms; + private final List plusTwoDayTerms; + private final List minusOneDayTerms; + private final List minusTwoDayTerms; + + // The following three regexes only used in this configuration + // They are not used in the base parser, therefore they are not extracted + // If the spanish date parser need the same regexes, they should be extracted + private final Pattern relativeDayRegex; + private final Pattern nextPrefixRegex; + private final Pattern previousPrefixRegex; + + @Override + public String getDateTokenPrefix() { + return dateTokenPrefix; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public Iterable getDateRegexes() { + return dateRegexes; + } + + @Override + public Pattern getOnRegex() { + return onRegex; + } + + @Override + public Pattern getSpecialDayRegex() { + return specialDayRegex; + } + + @Override + public Pattern getSpecialDayWithNumRegex() { + return specialDayWithNumRegex; + } + + @Override + public Pattern getNextRegex() { + return nextRegex; + } + + @Override + public Pattern getThisRegex() { + return thisRegex; + } + + @Override + public Pattern getLastRegex() { + return lastRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return weekDayRegex; + } + + @Override + public Pattern getMonthRegex() { + return monthRegex; + } + + @Override + public Pattern getWeekDayOfMonthRegex() { + return weekDayOfMonthRegex; + } + + @Override + public Pattern getForTheRegex() { + return forTheRegex; + } + + @Override + public Pattern getWeekDayAndDayOfMonthRegex() { + return weekDayAndDayOfMonthRegex; + } + + @Override + public Pattern getRelativeMonthRegex() { + return relativeMonthRegex; + } + + @Override + public Pattern getStrictRelativeRegex() { + return strictRelativeRegex; + } + + @Override + public Pattern getYearSuffix() { + return yearSuffix; + } + + @Override + public Pattern getRelativeWeekDayRegex() { + return relativeWeekDayRegex; + } + + @Override + public Pattern getRelativeDayRegex() { + return relativeDayRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return nextPrefixRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return previousPrefixRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } + + @Override + public ImmutableMap getDayOfWeek() { + return dayOfWeek; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public List getSameDayTerms() { + return sameDayTerms; + } + + @Override + public List getPlusOneDayTerms() { + return plusOneDayTerms; + } + + @Override + public List getMinusOneDayTerms() { + return minusOneDayTerms; + } + + @Override + public List getPlusTwoDayTerms() { + return plusTwoDayTerms; + } + + @Override + public List getMinusTwoDayTerms() { + return minusTwoDayTerms; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public Integer getSwiftMonthOrYear(String text) { + return getSwift(text); + } + + private Integer getSwift(String text) { + + String trimmedText = text.trim().toLowerCase(); + Integer swift = 0; + + Optional matchNext = Arrays.stream(RegExpUtility.getMatches(nextPrefixRegex, trimmedText)).findFirst(); + Optional matchPast = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)).findFirst(); + + if (matchNext.isPresent()) { + swift = 1; + } else if (matchPast.isPresent()) { + swift = -1; + } + + return swift; + } + + @Override + public Boolean isCardinalLast(String text) { + String trimmedText = text.trim().toLowerCase(); + return trimmedText.equals("last"); + } + + @Override + public String normalize(String text) { + return text; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDatePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDatePeriodParserConfiguration.java new file mode 100644 index 000000000..ff24cd50d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDatePeriodParserConfiguration.java @@ -0,0 +1,575 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDatePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class EnglishDatePeriodParserConfiguration extends BaseOptionsConfiguration implements IDatePeriodParserConfiguration { + + public EnglishDatePeriodParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + tokenBeforeDate = EnglishDateTime.TokenBeforeDate; + + cardinalExtractor = config.getCardinalExtractor(); + ordinalExtractor = config.getOrdinalExtractor(); + integerExtractor = config.getIntegerExtractor(); + numberParser = config.getNumberParser(); + dateExtractor = config.getDateExtractor(); + durationExtractor = config.getDurationExtractor(); + durationParser = config.getDurationParser(); + dateParser = config.getDateParser(); + + monthFrontBetweenRegex = EnglishDatePeriodExtractorConfiguration.MonthFrontBetweenRegex; + betweenRegex = EnglishDatePeriodExtractorConfiguration.BetweenRegex; + monthFrontSimpleCasesRegex = EnglishDatePeriodExtractorConfiguration.MonthFrontSimpleCasesRegex; + simpleCasesRegex = EnglishDatePeriodExtractorConfiguration.SimpleCasesRegex; + oneWordPeriodRegex = EnglishDatePeriodExtractorConfiguration.OneWordPeriodRegex; + monthWithYear = EnglishDatePeriodExtractorConfiguration.MonthWithYear; + monthNumWithYear = EnglishDatePeriodExtractorConfiguration.MonthNumWithYear; + yearRegex = EnglishDatePeriodExtractorConfiguration.YearRegex; + pastRegex = EnglishDatePeriodExtractorConfiguration.PreviousPrefixRegex; + futureRegex = EnglishDatePeriodExtractorConfiguration.NextPrefixRegex; + futureSuffixRegex = EnglishDatePeriodExtractorConfiguration.FutureSuffixRegex; + numberCombinedWithUnit = EnglishDurationExtractorConfiguration.NumberCombinedWithDurationUnit; + weekOfMonthRegex = EnglishDatePeriodExtractorConfiguration.WeekOfMonthRegex; + weekOfYearRegex = EnglishDatePeriodExtractorConfiguration.WeekOfYearRegex; + quarterRegex = EnglishDatePeriodExtractorConfiguration.QuarterRegex; + quarterRegexYearFront = EnglishDatePeriodExtractorConfiguration.QuarterRegexYearFront; + allHalfYearRegex = EnglishDatePeriodExtractorConfiguration.AllHalfYearRegex; + seasonRegex = EnglishDatePeriodExtractorConfiguration.SeasonRegex; + whichWeekRegex = EnglishDatePeriodExtractorConfiguration.WhichWeekRegex; + weekOfRegex = EnglishDatePeriodExtractorConfiguration.WeekOfRegex; + monthOfRegex = EnglishDatePeriodExtractorConfiguration.MonthOfRegex; + restOfDateRegex = EnglishDatePeriodExtractorConfiguration.RestOfDateRegex; + laterEarlyPeriodRegex = EnglishDatePeriodExtractorConfiguration.LaterEarlyPeriodRegex; + weekWithWeekDayRangeRegex = EnglishDatePeriodExtractorConfiguration.WeekWithWeekDayRangeRegex; + yearPlusNumberRegex = EnglishDatePeriodExtractorConfiguration.YearPlusNumberRegex; + decadeWithCenturyRegex = EnglishDatePeriodExtractorConfiguration.DecadeWithCenturyRegex; + yearPeriodRegex = EnglishDatePeriodExtractorConfiguration.YearPeriodRegex; + complexDatePeriodRegex = EnglishDatePeriodExtractorConfiguration.ComplexDatePeriodRegex; + relativeDecadeRegex = EnglishDatePeriodExtractorConfiguration.RelativeDecadeRegex; + inConnectorRegex = config.getUtilityConfiguration().getInConnectorRegex(); + withinNextPrefixRegex = EnglishDatePeriodExtractorConfiguration.WithinNextPrefixRegex; + referenceDatePeriodRegex = EnglishDatePeriodExtractorConfiguration.ReferenceDatePeriodRegex; + agoRegex = EnglishDatePeriodExtractorConfiguration.AgoRegex; + laterRegex = EnglishDatePeriodExtractorConfiguration.LaterRegex; + lessThanRegex = EnglishDatePeriodExtractorConfiguration.LessThanRegex; + moreThanRegex = EnglishDatePeriodExtractorConfiguration.MoreThanRegex; + centurySuffixRegex = EnglishDatePeriodExtractorConfiguration.CenturySuffixRegex; + relativeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RelativeRegex); + unspecificEndOfRangeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.UnspecificEndOfRangeRegex); + nowRegex = EnglishDatePeriodExtractorConfiguration.NowRegex; + + unitMap = config.getUnitMap(); + cardinalMap = config.getCardinalMap(); + dayOfMonth = config.getDayOfMonth(); + monthOfYear = config.getMonthOfYear(); + seasonMap = config.getSeasonMap(); + specialYearPrefixesMap = config.getSpecialYearPrefixesMap(); + writtenDecades = config.getWrittenDecades(); + numbers = config.getNumbers(); + specialDecadeCases = config.getSpecialDecadeCases(); + + nextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NextPrefixRegex); + previousPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PreviousPrefixRegex); + thisPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.ThisPrefixRegex); + afterNextSuffixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AfterNextSuffixRegex); + } + + private final String tokenBeforeDate; + + // InternalParsers + + private final IDateExtractor dateExtractor; + private final IExtractor cardinalExtractor; + private final IExtractor ordinalExtractor; + private final IDateTimeExtractor durationExtractor; + private final IExtractor integerExtractor; + private final IParser numberParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser durationParser; + + // Regex + + private final Pattern monthFrontBetweenRegex; + private final Pattern betweenRegex; + private final Pattern monthFrontSimpleCasesRegex; + private final Pattern simpleCasesRegex; + private final Pattern oneWordPeriodRegex; + private final Pattern monthWithYear; + private final Pattern monthNumWithYear; + private final Pattern yearRegex; + private final Pattern pastRegex; + private final Pattern futureRegex; + private final Pattern futureSuffixRegex; + private final Pattern numberCombinedWithUnit; + private final Pattern weekOfMonthRegex; + private final Pattern weekOfYearRegex; + private final Pattern quarterRegex; + private final Pattern quarterRegexYearFront; + private final Pattern allHalfYearRegex; + private final Pattern seasonRegex; + private final Pattern whichWeekRegex; + private final Pattern weekOfRegex; + private final Pattern monthOfRegex; + private final Pattern inConnectorRegex; + private final Pattern withinNextPrefixRegex; + private final Pattern restOfDateRegex; + private final Pattern laterEarlyPeriodRegex; + private final Pattern weekWithWeekDayRangeRegex; + private final Pattern yearPlusNumberRegex; + private final Pattern decadeWithCenturyRegex; + private final Pattern yearPeriodRegex; + private final Pattern complexDatePeriodRegex; + private final Pattern relativeDecadeRegex; + private final Pattern referenceDatePeriodRegex; + private final Pattern agoRegex; + private final Pattern laterRegex; + private final Pattern lessThanRegex; + private final Pattern moreThanRegex; + private final Pattern centurySuffixRegex; + private final Pattern relativeRegex; + private final Pattern unspecificEndOfRangeRegex; + private final Pattern nextPrefixRegex; + private final Pattern previousPrefixRegex; + private final Pattern thisPrefixRegex; + private final Pattern afterNextSuffixRegex; + private final Pattern nowRegex; + + // Dictionaries + private final ImmutableMap unitMap; + private final ImmutableMap cardinalMap; + private final ImmutableMap dayOfMonth; + private final ImmutableMap monthOfYear; + private final ImmutableMap seasonMap; + private final ImmutableMap specialYearPrefixesMap; + private final ImmutableMap writtenDecades; + private final ImmutableMap numbers; + private final ImmutableMap specialDecadeCases; + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public Pattern getMonthFrontBetweenRegex() { + return monthFrontBetweenRegex; + } + + @Override + public Pattern getBetweenRegex() { + return betweenRegex; + } + + @Override + public Pattern getMonthFrontSimpleCasesRegex() { + return monthFrontSimpleCasesRegex; + } + + @Override + public Pattern getSimpleCasesRegex() { + return simpleCasesRegex; + } + + @Override + public Pattern getOneWordPeriodRegex() { + return oneWordPeriodRegex; + } + + @Override + public Pattern getMonthWithYear() { + return monthWithYear; + } + + @Override + public Pattern getMonthNumWithYear() { + return monthNumWithYear; + } + + @Override + public Pattern getYearRegex() { + return yearRegex; + } + + @Override + public Pattern getPastRegex() { + return pastRegex; + } + + @Override + public Pattern getFutureRegex() { + return futureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return futureSuffixRegex; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return numberCombinedWithUnit; + } + + @Override + public Pattern getWeekOfMonthRegex() { + return weekOfMonthRegex; + } + + @Override + public Pattern getWeekOfYearRegex() { + return weekOfYearRegex; + } + + @Override + public Pattern getQuarterRegex() { + return quarterRegex; + } + + @Override + public Pattern getQuarterRegexYearFront() { + return quarterRegexYearFront; + } + + @Override + public Pattern getAllHalfYearRegex() { + return allHalfYearRegex; + } + + @Override + public Pattern getSeasonRegex() { + return seasonRegex; + } + + @Override + public Pattern getWhichWeekRegex() { + return whichWeekRegex; + } + + @Override + public Pattern getWeekOfRegex() { + return weekOfRegex; + } + + @Override + public Pattern getMonthOfRegex() { + return monthOfRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return inConnectorRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return withinNextPrefixRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return nextPrefixRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return previousPrefixRegex; + } + + @Override + public Pattern getThisPrefixRegex() { + return thisPrefixRegex; + } + + @Override + public Pattern getRestOfDateRegex() { + return restOfDateRegex; + } + + @Override + public Pattern getLaterEarlyPeriodRegex() { + return laterEarlyPeriodRegex; + } + + @Override + public Pattern getWeekWithWeekDayRangeRegex() { + return weekWithWeekDayRangeRegex; + } + + @Override + public Pattern getYearPlusNumberRegex() { + return yearPlusNumberRegex; + } + + @Override + public Pattern getDecadeWithCenturyRegex() { + return decadeWithCenturyRegex; + } + + @Override + public Pattern getYearPeriodRegex() { + return yearPeriodRegex; + } + + @Override + public Pattern getComplexDatePeriodRegex() { + return complexDatePeriodRegex; + } + + @Override + public Pattern getRelativeDecadeRegex() { + return relativeDecadeRegex; + } + + @Override + public Pattern getReferenceDatePeriodRegex() { + return referenceDatePeriodRegex; + } + + @Override + public Pattern getAgoRegex() { + return agoRegex; + } + + @Override + public Pattern getLaterRegex() { + return laterRegex; + } + + @Override + public Pattern getLessThanRegex() { + return lessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return moreThanRegex; + } + + @Override + public Pattern getCenturySuffixRegex() { + return centurySuffixRegex; + } + + @Override + public Pattern getRelativeRegex() { + return relativeRegex; + } + + @Override + public Pattern getUnspecificEndOfRangeRegex() { + return unspecificEndOfRangeRegex; + } + + @Override + public Pattern getNowRegex() { + return nowRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getSeasonMap() { + return seasonMap; + } + + @Override + public ImmutableMap getSpecialYearPrefixesMap() { + return specialYearPrefixesMap; + } + + @Override + public ImmutableMap getWrittenDecades() { + return writtenDecades; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public ImmutableMap getSpecialDecadeCases() { + return specialDecadeCases; + } + + @Override + public int getSwiftDayOrMonth(String text) { + + String trimmedText = text.trim().toLowerCase(); + int swift = 0; + + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + Optional matchNext = Arrays.stream(RegExpUtility.getMatches(nextPrefixRegex, trimmedText)).findFirst(); + Optional matchPast = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)).findFirst(); + + if (matchAfterNext.isPresent()) { + swift = 2; + } else if (matchNext.isPresent()) { + swift = 1; + } else if (matchPast.isPresent()) { + swift = -1; + } + + return swift; + } + + @Override + public int getSwiftYear(String text) { + + String trimmedText = text.trim().toLowerCase(); + int swift = -10; + + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + Optional matchNext = Arrays.stream(RegExpUtility.getMatches(nextPrefixRegex, trimmedText)).findFirst(); + Optional matchPast = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)).findFirst(); + Optional matchThisPresent = Arrays.stream(RegExpUtility.getMatches(thisPrefixRegex, trimmedText)).findFirst(); + + if (matchAfterNext.isPresent()) { + swift = 2; + } else if (matchNext.isPresent()) { + swift = 1; + } else if (matchPast.isPresent()) { + swift = -1; + } else if (matchThisPresent.isPresent()) { + swift = 0; + } + + return swift; + } + + @Override + public boolean isFuture(String text) { + String trimmedText = text.trim().toLowerCase(); + return (trimmedText.startsWith("this") || trimmedText.startsWith("next")); + } + + @Override + public boolean isLastCardinal(String text) { + String trimmedText = text.trim().toLowerCase(); + return trimmedText.equals("last"); + } + + @Override + public boolean isMonthOnly(String text) { + String trimmedText = text.trim().toLowerCase(); + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + return trimmedText.endsWith("month") || trimmedText.contains(" month ") && matchAfterNext.isPresent(); + } + + @Override + public boolean isMonthToDate(String text) { + String trimmedText = text.trim().toLowerCase(); + return trimmedText.equals("month to date"); + } + + @Override + public boolean isWeekend(String text) { + String trimmedText = text.trim().toLowerCase(); + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + return trimmedText.endsWith("weekend") || trimmedText.contains(" weekend ") && matchAfterNext.isPresent(); + } + + @Override + public boolean isWeekOnly(String text) { + String trimmedText = text.trim().toLowerCase(); + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + return trimmedText.endsWith("week") || trimmedText.contains(" week ") && matchAfterNext.isPresent(); + } + + @Override + public boolean isYearOnly(String text) { + String trimmedText = text.trim().toLowerCase(); + return EnglishDateTime.YearTerms.stream().anyMatch(o -> trimmedText.endsWith(o)) || + (getYearTermsPadded().anyMatch(o -> trimmedText.contains(o)) && RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText).length > 0) || + (EnglishDateTime.GenericYearTerms.stream().anyMatch(o -> trimmedText.endsWith(o)) && RegExpUtility.getMatches(unspecificEndOfRangeRegex, trimmedText).length > 0); + } + + @Override + public boolean isYearToDate(String text) { + String trimmedText = text.trim().toLowerCase(); + return trimmedText.equals("year to date"); + } + + private Stream getYearTermsPadded() { + return EnglishDateTime.YearTerms.stream().map(i -> String.format(" %s ", i)); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimeAltParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimeAltParserConfiguration.java new file mode 100644 index 000000000..158464df9 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimeAltParserConfiguration.java @@ -0,0 +1,48 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeAltParserConfiguration; + +public class EnglishDateTimeAltParserConfiguration implements IDateTimeAltParserConfiguration { + + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimePeriodParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser datePeriodParser; + + public EnglishDateTimeAltParserConfiguration(ICommonDateTimeParserConfiguration config) { + dateTimeParser = config.getDateTimeParser(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + dateTimePeriodParser = config.getDateTimePeriodParser(); + timePeriodParser = config.getTimePeriodParser(); + datePeriodParser = config.getDatePeriodParser(); + } + + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + public IDateTimeParser getDateParser() { + return dateParser; + } + + public IDateTimeParser getTimeParser() { + return timeParser; + } + + public IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + public IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimeParserConfiguration.java new file mode 100644 index 000000000..aa285aa9e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimeParserConfiguration.java @@ -0,0 +1,256 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultTimex; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class EnglishDateTimeParserConfiguration extends BaseOptionsConfiguration implements IDateTimeParserConfiguration { + + private final String tokenBeforeDate; + private final String tokenBeforeTime; + + private final IDateTimeExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IExtractor cardinalExtractor; + private final IExtractor integerExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeParser durationParser; + + private final Pattern nowRegex; + private final Pattern amTimeRegex; + private final Pattern pmTimeRegex; + private final Pattern simpleTimeOfTodayAfterRegex; + private final Pattern simpleTimeOfTodayBeforeRegex; + private final Pattern specificTimeOfDayRegex; + private final Pattern specificEndOfRegex; + private final Pattern unspecificEndOfRegex; + private final Pattern unitRegex; + private final Pattern dateNumberConnectorRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap numbers; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + public EnglishDateTimeParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + tokenBeforeDate = EnglishDateTime.TokenBeforeDate; + tokenBeforeTime = EnglishDateTime.TokenBeforeTime; + + cardinalExtractor = config.getCardinalExtractor(); + integerExtractor = config.getIntegerExtractor(); + numberParser = config.getNumberParser(); + dateExtractor = config.getDateExtractor(); + timeExtractor = config.getTimeExtractor(); + durationExtractor = config.getDurationExtractor(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + durationParser = config.getDurationParser(); + + nowRegex = EnglishDateTimeExtractorConfiguration.NowRegex; + + amTimeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AMTimeRegex); + pmTimeRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PMTimeRegex); + + simpleTimeOfTodayAfterRegex = EnglishDateTimeExtractorConfiguration.SimpleTimeOfTodayAfterRegex; + simpleTimeOfTodayBeforeRegex = EnglishDateTimeExtractorConfiguration.SimpleTimeOfTodayBeforeRegex; + specificTimeOfDayRegex = EnglishDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + specificEndOfRegex = EnglishDateTimeExtractorConfiguration.SpecificEndOfRegex; + unspecificEndOfRegex = EnglishDateTimeExtractorConfiguration.UnspecificEndOfRegex; + unitRegex = EnglishTimeExtractorConfiguration.TimeUnitRegex; + dateNumberConnectorRegex = EnglishDateTimeExtractorConfiguration.DateNumberConnectorRegex; + + unitMap = config.getUnitMap(); + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public String getTokenBeforeTime() { + return tokenBeforeTime; + } + + @Override + public IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public Pattern getNowRegex() { + return nowRegex; + } + + @Override + public Pattern getAMTimeRegex() { + return amTimeRegex; + } + + @Override + public Pattern getPMTimeRegex() { + return pmTimeRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayAfterRegex() { + return simpleTimeOfTodayAfterRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayBeforeRegex() { + return simpleTimeOfTodayBeforeRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return specificTimeOfDayRegex; + } + + @Override + public Pattern getSpecificEndOfRegex() { + return specificEndOfRegex; + } + + @Override + public Pattern getUnspecificEndOfRegex() { + return unspecificEndOfRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getDateNumberConnectorRegex() { + return dateNumberConnectorRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public boolean containsAmbiguousToken(String text, String matchedText) { + return false; + } + + @Override + public ResultTimex getMatchedNowTimex(String text) { + + String trimmedText = text.trim().toLowerCase(); + + if (trimmedText.endsWith("now")) { + return new ResultTimex(true, "PRESENT_REF"); + } else if (trimmedText.equals("recently") || trimmedText.equals("previously")) { + return new ResultTimex(true, "PAST_REF"); + } else if (trimmedText.equals("as soon as possible") || trimmedText.equals("asap")) { + return new ResultTimex(true, "FUTURE_REF"); + } + + return new ResultTimex(false, null); + } + + @Override + public int getSwiftDay(String text) { + + String trimmedText = text.trim().toLowerCase(); + + int swift = 0; + if (trimmedText.startsWith("next")) { + swift = 1; + } else if (trimmedText.startsWith("last")) { + swift = -1; + } + + return swift; + } + + @Override + public int getHour(String text, int hour) { + + String trimmedText = text.trim().toLowerCase(); + int result = hour; + + if (trimmedText.endsWith("morning") && hour >= Constants.HalfDayHourCount) { + result -= Constants.HalfDayHourCount; + } else if (!trimmedText.endsWith("morning") && hour < Constants.HalfDayHourCount && !(trimmedText.endsWith("night") && hour < 6)) { + result += Constants.HalfDayHourCount; + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimePeriodParserConfiguration.java new file mode 100644 index 000000000..3e277e865 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDateTimePeriodParserConfiguration.java @@ -0,0 +1,337 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class EnglishDateTimePeriodParserConfiguration extends BaseOptionsConfiguration implements IDateTimePeriodParserConfiguration { + + private final String tokenBeforeDate; + + private final IDateTimeExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor durationExtractor; + private final IExtractor cardinalExtractor; + + private final IParser numberParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser durationParser; + private final IDateTimeParser timeZoneParser; + + private final Pattern pureNumberFromToRegex; + private final Pattern pureNumberBetweenAndRegex; + private final Pattern specificTimeOfDayRegex; + private final Pattern timeOfDayRegex; + private final Pattern pastRegex; + private final Pattern futureRegex; + private final Pattern futureSuffixRegex; + private final Pattern numberCombinedWithUnitRegex; + private final Pattern unitRegex; + private final Pattern periodTimeOfDayWithDateRegex; + private final Pattern relativeTimeUnitRegex; + private final Pattern restOfDateTimeRegex; + private final Pattern amDescRegex; + private final Pattern pmDescRegex; + private final Pattern withinNextPrefixRegex; + private final Pattern prefixDayRegex; + private final Pattern beforeRegex; + private final Pattern afterRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap numbers; + + public static final Pattern MorningStartEndRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.MorningStartEndRegex); + public static final Pattern AfternoonStartEndRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AfternoonStartEndRegex); + public static final Pattern EveningStartEndRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.EveningStartEndRegex); + public static final Pattern NightStartEndRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NightStartEndRegex); + + public EnglishDateTimePeriodParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + tokenBeforeDate = EnglishDateTime.TokenBeforeDate; + + dateExtractor = config.getDateExtractor(); + timeExtractor = config.getTimeExtractor(); + dateTimeExtractor = config.getDateTimeExtractor(); + timePeriodExtractor = config.getTimePeriodExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + durationExtractor = config.getDurationExtractor(); + numberParser = config.getNumberParser(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + timePeriodParser = config.getTimePeriodParser(); + durationParser = config.getDurationParser(); + dateTimeParser = config.getDateTimeParser(); + timeZoneParser = config.getTimeZoneParser(); + + pureNumberFromToRegex = EnglishTimePeriodExtractorConfiguration.PureNumFromTo; + pureNumberBetweenAndRegex = EnglishTimePeriodExtractorConfiguration.PureNumBetweenAnd; + specificTimeOfDayRegex = EnglishDateTimePeriodExtractorConfiguration.PeriodSpecificTimeOfDayRegex; + timeOfDayRegex = EnglishDateTimeExtractorConfiguration.TimeOfDayRegex; + pastRegex = EnglishDatePeriodExtractorConfiguration.PreviousPrefixRegex; + futureRegex = EnglishDatePeriodExtractorConfiguration.NextPrefixRegex; + futureSuffixRegex = EnglishDatePeriodExtractorConfiguration.FutureSuffixRegex; + numberCombinedWithUnitRegex = EnglishDateTimePeriodExtractorConfiguration.TimeNumberCombinedWithUnit; + unitRegex = EnglishTimePeriodExtractorConfiguration.TimeUnitRegex; + periodTimeOfDayWithDateRegex = EnglishDateTimePeriodExtractorConfiguration.PeriodTimeOfDayWithDateRegex; + relativeTimeUnitRegex = EnglishDateTimePeriodExtractorConfiguration.RelativeTimeUnitRegex; + restOfDateTimeRegex = EnglishDateTimePeriodExtractorConfiguration.RestOfDateTimeRegex; + amDescRegex = EnglishDateTimePeriodExtractorConfiguration.AmDescRegex; + pmDescRegex = EnglishDateTimePeriodExtractorConfiguration.PmDescRegex; + withinNextPrefixRegex = EnglishDateTimePeriodExtractorConfiguration.WithinNextPrefixRegex; + prefixDayRegex = EnglishDateTimePeriodExtractorConfiguration.PrefixDayRegex; + beforeRegex = EnglishDateTimePeriodExtractorConfiguration.BeforeRegex; + afterRegex = EnglishDateTimePeriodExtractorConfiguration.AfterRegex; + + unitMap = config.getUnitMap(); + numbers = config.getNumbers(); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + @Override + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public Pattern getPureNumberFromToRegex() { + return pureNumberFromToRegex; + } + + @Override + public Pattern getPureNumberBetweenAndRegex() { + return pureNumberBetweenAndRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return specificTimeOfDayRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return timeOfDayRegex; + } + + @Override + public Pattern getPastRegex() { + return pastRegex; + } + + @Override + public Pattern getFutureRegex() { + return futureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return futureSuffixRegex; + } + + @Override + public Pattern getNumberCombinedWithUnitRegex() { + return numberCombinedWithUnitRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getPeriodTimeOfDayWithDateRegex() { + return periodTimeOfDayWithDateRegex; + } + + @Override + public Pattern getRelativeTimeUnitRegex() { + return relativeTimeUnitRegex; + } + + @Override + public Pattern getRestOfDateTimeRegex() { + return restOfDateTimeRegex; + } + + @Override + public Pattern getAmDescRegex() { + return amDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return pmDescRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return withinNextPrefixRegex; + } + + @Override + public Pattern getPrefixDayRegex() { + return prefixDayRegex; + } + + @Override + public Pattern getBeforeRegex() { + return beforeRegex; + } + + @Override + public Pattern getAfterRegex() { + return afterRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + private boolean checkRegex(Pattern regex, String input) { + return RegExpUtility.getMatches(regex, input).length > 0; + } + + @Override + public MatchedTimeRangeResult getMatchedTimeRange(String text, String timeStr, int beginHour, int endHour, int endMin) { + + String trimmedText = text.trim().toLowerCase(); + beginHour = 0; + endHour = 0; + endMin = 0; + timeStr = null; + boolean result = false; + + if (checkRegex(MorningStartEndRegex, trimmedText)) { + timeStr = "TMO"; + beginHour = 8; + endHour = Constants.HalfDayHourCount; + result = true; + } else if (checkRegex(AfternoonStartEndRegex, trimmedText)) { + timeStr = "TAF"; + beginHour = Constants.HalfDayHourCount; + endHour = 16; + result = true; + } else if (checkRegex(EveningStartEndRegex, trimmedText)) { + timeStr = "TEV"; + beginHour = 16; + endHour = 20; + result = true; + } else if (checkRegex(NightStartEndRegex, trimmedText)) { + timeStr = "TNI"; + beginHour = 20; + endHour = 23; + endMin = 59; + result = true; + } else { + timeStr = null; + } + + return new MatchedTimeRangeResult(result, timeStr, beginHour, endHour, endMin); + } + + @Override + public int getSwiftPrefix(String text) { + + String trimmedText = text.trim().toLowerCase(); + + int swift = 0; + if (trimmedText.startsWith("next")) { + swift = 1; + } else if (trimmedText.startsWith("last")) { + swift = -1; + } + + return swift; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDurationParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDurationParserConfiguration.java new file mode 100644 index 000000000..5a453bd75 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishDurationParserConfiguration.java @@ -0,0 +1,145 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDurationParserConfiguration; + +import java.util.regex.Pattern; + +public class EnglishDurationParserConfiguration extends BaseOptionsConfiguration implements IDurationParserConfiguration { + + private final IExtractor cardinalExtractor; + private final IExtractor durationExtractor; + private final IParser numberParser; + + private final Pattern numberCombinedWithUnit; + private final Pattern anUnitRegex; + private final Pattern duringRegex; + private final Pattern allDateUnitRegex; + private final Pattern halfDateUnitRegex; + private final Pattern suffixAndRegex; + private final Pattern followedUnit; + private final Pattern conjunctionRegex; + private final Pattern inexactNumberRegex; + private final Pattern inexactNumberUnitRegex; + private final Pattern durationUnitRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + private final ImmutableMap doubleNumbers; + + public EnglishDurationParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + cardinalExtractor = config.getCardinalExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = new BaseDurationExtractor(new EnglishDurationExtractorConfiguration(), false); + numberCombinedWithUnit = EnglishDurationExtractorConfiguration.NumberCombinedWithDurationUnit; + + anUnitRegex = EnglishDurationExtractorConfiguration.AnUnitRegex; + duringRegex = EnglishDurationExtractorConfiguration.DuringRegex; + allDateUnitRegex = EnglishDurationExtractorConfiguration.AllRegex; + halfDateUnitRegex = EnglishDurationExtractorConfiguration.HalfRegex; + suffixAndRegex = EnglishDurationExtractorConfiguration.SuffixAndRegex; + followedUnit = EnglishDurationExtractorConfiguration.DurationFollowedUnit; + conjunctionRegex = EnglishDurationExtractorConfiguration.ConjunctionRegex; + inexactNumberRegex = EnglishDurationExtractorConfiguration.InexactNumberRegex; + inexactNumberUnitRegex = EnglishDurationExtractorConfiguration.InexactNumberUnitRegex; + durationUnitRegex = EnglishDurationExtractorConfiguration.DurationUnitRegex; + + unitMap = config.getUnitMap(); + unitValueMap = config.getUnitValueMap(); + doubleNumbers = config.getDoubleNumbers(); + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return numberCombinedWithUnit; + } + + @Override + public Pattern getAnUnitRegex() { + return anUnitRegex; + } + + @Override + public Pattern getDuringRegex() { + return duringRegex; + } + + @Override + public Pattern getAllDateUnitRegex() { + return allDateUnitRegex; + } + + @Override + public Pattern getHalfDateUnitRegex() { + return halfDateUnitRegex; + } + + @Override + public Pattern getSuffixAndRegex() { + return suffixAndRegex; + } + + @Override + public Pattern getFollowedUnit() { + return followedUnit; + } + + @Override + public Pattern getConjunctionRegex() { + return conjunctionRegex; + } + + @Override + public Pattern getInexactNumberRegex() { + return inexactNumberRegex; + } + + @Override + public Pattern getInexactNumberUnitRegex() { + return inexactNumberUnitRegex; + } + + @Override + public Pattern getDurationUnitRegex() { + return durationUnitRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } + + @Override + public ImmutableMap getDoubleNumbers() { + return doubleNumbers; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishHolidayParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishHolidayParserConfiguration.java new file mode 100644 index 000000000..bbac800b0 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishHolidayParserConfiguration.java @@ -0,0 +1,225 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseHolidayParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.IntFunction; + +public class EnglishHolidayParserConfiguration extends BaseHolidayParserConfiguration { + + public EnglishHolidayParserConfiguration() { + + super(); + + this.setHolidayRegexList(EnglishHolidayExtractorConfiguration.HolidayRegexList); + + HashMap> newMap = new HashMap<>(); + for (Map.Entry entry : EnglishDateTime.HolidayNames.entrySet()) { + if (entry.getValue() instanceof String[]) { + newMap.put(entry.getKey(), Arrays.asList(entry.getValue())); + } + } + this.setHolidayNames(ImmutableMap.copyOf(newMap)); + } + + @Override + protected HashMap> initHolidayFuncs() { + + HashMap> holidays = new HashMap<>(super.initHolidayFuncs()); + holidays.put("mayday", EnglishHolidayParserConfiguration::mayday); + holidays.put("yuandan", EnglishHolidayParserConfiguration::newYear); + holidays.put("newyear", EnglishHolidayParserConfiguration::newYear); + holidays.put("youthday", EnglishHolidayParserConfiguration::youthDay); + holidays.put("girlsday", EnglishHolidayParserConfiguration::girlsDay); + holidays.put("xmas", EnglishHolidayParserConfiguration::christmasDay); + holidays.put("newyearday", EnglishHolidayParserConfiguration::newYear); + holidays.put("aprilfools", EnglishHolidayParserConfiguration::foolDay); + holidays.put("easterday", EnglishHolidayParserConfiguration::easterDay); + holidays.put("newyearsday", EnglishHolidayParserConfiguration::newYear); + holidays.put("femaleday", EnglishHolidayParserConfiguration::femaleDay); + holidays.put("singleday", EnglishHolidayParserConfiguration::singlesDay); + holidays.put("newyeareve", EnglishHolidayParserConfiguration::newYearEve); + holidays.put("arborday", EnglishHolidayParserConfiguration::treePlantDay); + holidays.put("loverday", EnglishHolidayParserConfiguration::valentinesDay); + holidays.put("christmas", EnglishHolidayParserConfiguration::christmasDay); + holidays.put("teachersday", EnglishHolidayParserConfiguration::teacherDay); + holidays.put("stgeorgeday", EnglishHolidayParserConfiguration::stGeorgeDay); + holidays.put("baptisteday", EnglishHolidayParserConfiguration::baptisteDay); + holidays.put("bastilleday", EnglishHolidayParserConfiguration::bastilleDay); + holidays.put("allsoulsday", EnglishHolidayParserConfiguration::allSoulsDay); + holidays.put("veteransday", EnglishHolidayParserConfiguration::veteransDay); + holidays.put("childrenday", EnglishHolidayParserConfiguration::childrenDay); + holidays.put("maosbirthday", EnglishHolidayParserConfiguration::maoBirthday); + holidays.put("allsaintsday", EnglishHolidayParserConfiguration::halloweenDay); + holidays.put("stpatrickday", EnglishHolidayParserConfiguration::stPatrickDay); + holidays.put("halloweenday", EnglishHolidayParserConfiguration::halloweenDay); + holidays.put("allhallowday", EnglishHolidayParserConfiguration::allHallowDay); + holidays.put("guyfawkesday", EnglishHolidayParserConfiguration::guyFawkesDay); + holidays.put("christmaseve", EnglishHolidayParserConfiguration::christmasEve); + holidays.put("groundhougday", EnglishHolidayParserConfiguration::groundhogDay); + holidays.put("whiteloverday", EnglishHolidayParserConfiguration::whiteLoverDay); + holidays.put("valentinesday", EnglishHolidayParserConfiguration::valentinesDay); + holidays.put("treeplantingday", EnglishHolidayParserConfiguration::treePlantDay); + holidays.put("cincodemayoday", EnglishHolidayParserConfiguration::cincoDeMayoDay); + holidays.put("inaugurationday", EnglishHolidayParserConfiguration::inaugurationDay); + holidays.put("independenceday", EnglishHolidayParserConfiguration::usaIndependenceDay); + holidays.put("usindependenceday", EnglishHolidayParserConfiguration::usaIndependenceDay); + holidays.put("juneteenth", EnglishHolidayParserConfiguration::juneteenth); + + return holidays; + } + + private static LocalDateTime easterDay(int year) { + return DateUtil.minValue(); + } + + private static LocalDateTime mayday(int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 1); + } + + private static LocalDateTime newYear(int year) { + return DateUtil.safeCreateFromMinValue(year, 1, 1); + } + + private static LocalDateTime foolDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 4, 1); + } + + private static LocalDateTime girlsDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 7); + } + + private static LocalDateTime youthDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 4); + } + + private static LocalDateTime femaleDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 8); + } + + private static LocalDateTime childrenDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 1); + } + + private static LocalDateTime teacherDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 9, 10); + } + + private static LocalDateTime groundhogDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 2, 2); + } + + private static LocalDateTime stGeorgeDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 4, 23); + } + + private static LocalDateTime baptisteDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 24); + } + + private static LocalDateTime bastilleDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 7, 14); + } + + private static LocalDateTime allSoulsDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 2); + } + + private static LocalDateTime singlesDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 11); + } + + private static LocalDateTime newYearEve(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 31); + } + + private static LocalDateTime treePlantDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 12); + } + + private static LocalDateTime stPatrickDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 17); + } + + private static LocalDateTime allHallowDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 1); + } + + private static LocalDateTime guyFawkesDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 5); + } + + private static LocalDateTime veteransDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 11); + } + + private static LocalDateTime maoBirthday(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 26); + } + + private static LocalDateTime valentinesDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 2, 14); + } + + private static LocalDateTime whiteLoverDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 14); + } + + private static LocalDateTime cincoDeMayoDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 5); + } + + private static LocalDateTime halloweenDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 10, 31); + } + + private static LocalDateTime christmasEve(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 24); + } + + private static LocalDateTime christmasDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 25); + } + + private static LocalDateTime inaugurationDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 1, 20); + } + + private static LocalDateTime usaIndependenceDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 7, 4); + } + + private static LocalDateTime juneteenth(int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 19); + } + + @Override + public int getSwiftYear(String text) { + + String trimmedText = StringUtility.trimStart(StringUtility.trimEnd(text)).toLowerCase(Locale.ROOT); + int swift = -10; + + if (trimmedText.startsWith("next")) { + swift = 1; + } else if (trimmedText.startsWith("last")) { + swift = -1; + } else if (trimmedText.startsWith("this")) { + swift = 0; + } + + return swift; + } + + public String sanitizeHolidayToken(String holiday) { + return holiday.replace(" ", "").replace("'", ""); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishMergedParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishMergedParserConfiguration.java new file mode 100644 index 000000000..b8b43c163 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishMergedParserConfiguration.java @@ -0,0 +1,77 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseHolidayParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseSetParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.IMergedParserConfiguration; +import com.microsoft.recognizers.text.matcher.StringMatcher; + +import java.util.regex.Pattern; + +public class EnglishMergedParserConfiguration extends EnglishCommonDateTimeParserConfiguration implements IMergedParserConfiguration { + + public EnglishMergedParserConfiguration(DateTimeOptions options) { + super(options); + + beforeRegex = EnglishMergedExtractorConfiguration.BeforeRegex; + afterRegex = EnglishMergedExtractorConfiguration.AfterRegex; + sinceRegex = EnglishMergedExtractorConfiguration.SinceRegex; + aroundRegex = EnglishMergedExtractorConfiguration.AroundRegex; + suffixAfterRegex = EnglishMergedExtractorConfiguration.SuffixAfterRegex; + yearRegex = EnglishDatePeriodExtractorConfiguration.YearRegex; + superfluousWordMatcher = EnglishMergedExtractorConfiguration.SuperfluousWordMatcher; + + getParser = new BaseSetParser(new EnglishSetParserConfiguration(this)); + holidayParser = new BaseHolidayParser(new EnglishHolidayParserConfiguration()); + } + + private final Pattern beforeRegex; + private final Pattern afterRegex; + private final Pattern sinceRegex; + private final Pattern aroundRegex; + private final Pattern suffixAfterRegex; + private final Pattern yearRegex; + private final IDateTimeParser getParser; + private final IDateTimeParser holidayParser; + private final StringMatcher superfluousWordMatcher; + + public Pattern getBeforeRegex() { + return beforeRegex; + } + + public Pattern getAfterRegex() { + return afterRegex; + } + + public Pattern getSinceRegex() { + return sinceRegex; + } + + public Pattern getAroundRegex() { + return aroundRegex; + } + + public Pattern getSuffixAfterRegex() { + return suffixAfterRegex; + } + + public Pattern getYearRegex() { + return yearRegex; + } + + public IDateTimeParser getGetParser() { + return getParser; + } + + public IDateTimeParser getHolidayParser() { + return holidayParser; + } + + public StringMatcher getSuperfluousWordMatcher() { + return superfluousWordMatcher; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishSetParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishSetParserConfiguration.java new file mode 100644 index 000000000..290cd5a7f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishSetParserConfiguration.java @@ -0,0 +1,251 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishSetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ISetParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.MatchedTimexResult; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Locale; +import java.util.regex.Pattern; + +public class EnglishSetParserConfiguration extends BaseOptionsConfiguration implements ISetParserConfiguration { + + private IDateTimeParser timeParser; + + public final IDateTimeParser getTimeParser() { + return timeParser; + } + + private IDateTimeParser dateParser; + + public final IDateTimeParser getDateParser() { + return dateParser; + } + + private ImmutableMap unitMap; + + public final ImmutableMap getUnitMap() { + return unitMap; + } + + private IDateTimeParser dateTimeParser; + + public final IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + private IDateTimeParser durationParser; + + public final IDateTimeParser getDurationParser() { + return durationParser; + } + + private IDateTimeExtractor timeExtractor; + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + private IDateExtractor dateExtractor; + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + private IDateTimeParser datePeriodParser; + + public final IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } + + private IDateTimeParser timePeriodParser; + + public final IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor dateTimeExtractor; + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + private IDateTimeParser dateTimePeriodParser; + + public final IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + private IDateTimeExtractor datePeriodExtractor; + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + private IDateTimeExtractor timePeriodExtractor; + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + private IDateTimeExtractor dateTimePeriodExtractor; + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + private Pattern eachDayRegex; + + public final Pattern getEachDayRegex() { + return eachDayRegex; + } + + private Pattern setEachRegex; + + public final Pattern getSetEachRegex() { + return setEachRegex; + } + + private Pattern periodicRegex; + + public final Pattern getPeriodicRegex() { + return periodicRegex; + } + + private Pattern eachUnitRegex; + + public final Pattern getEachUnitRegex() { + return eachUnitRegex; + } + + private Pattern setWeekDayRegex; + + public final Pattern getSetWeekDayRegex() { + return setWeekDayRegex; + } + + private Pattern eachPrefixRegex; + + public final Pattern getEachPrefixRegex() { + return eachPrefixRegex; + } + + private static Pattern doubleMultiplierRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.DoubleMultiplierRegex); + + private static Pattern halfMultiplierRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.HalfMultiplierRegex); + + private static Pattern dayTypeRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.DayTypeRegex); + + private static Pattern weekTypeRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.WeekTypeRegex); + + private static Pattern weekendTypeRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.WeekendTypeRegex); + + private static Pattern monthTypeRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.MonthTypeRegex); + + private static Pattern quarterTypeRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.QuarterTypeRegex); + + private static Pattern yearTypeRegex = + RegExpUtility.getSafeRegExp(EnglishDateTime.YearTypeRegex); + + public EnglishSetParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + timeExtractor = config.getTimeExtractor(); + dateExtractor = config.getDateExtractor(); + dateTimeExtractor = config.getDateTimeExtractor(); + durationExtractor = config.getDurationExtractor(); + datePeriodExtractor = config.getDatePeriodExtractor(); + timePeriodExtractor = config.getTimePeriodExtractor(); + dateTimePeriodExtractor = config.getDateTimePeriodExtractor(); + + unitMap = config.getUnitMap(); + timeParser = config.getTimeParser(); + dateParser = config.getDateParser(); + dateTimeParser = config.getDateTimeParser(); + durationParser = config.getDurationParser(); + datePeriodParser = config.getDatePeriodParser(); + timePeriodParser = config.getTimePeriodParser(); + dateTimePeriodParser = config.getDateTimePeriodParser(); + + eachDayRegex = EnglishSetExtractorConfiguration.EachDayRegex; + setEachRegex = EnglishSetExtractorConfiguration.SetEachRegex; + eachUnitRegex = EnglishSetExtractorConfiguration.EachUnitRegex; + periodicRegex = EnglishSetExtractorConfiguration.PeriodicRegex; + eachPrefixRegex = EnglishSetExtractorConfiguration.EachPrefixRegex; + setWeekDayRegex = EnglishSetExtractorConfiguration.SetWeekDayRegex; + } + + public MatchedTimexResult getMatchedDailyTimex(String text) { + + MatchedTimexResult result = new MatchedTimexResult(); + + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + + float durationLength = 1; // Default value + float multiplier = 1; + String durationType; + + if (trimmedText.equals("daily")) { + result.setTimex("P1D"); + } else if (trimmedText.equals("weekly")) { + result.setTimex("P1W"); + } else if (trimmedText.equals("biweekly")) { + result.setTimex("P2W"); + } else if (trimmedText.equals("monthly")) { + result.setTimex("P1M"); + } else if (trimmedText.equals("quarterly")) { + result.setTimex("P3M"); + } else if (trimmedText.equals("yearly") || trimmedText.equals("annually") || trimmedText.equals("annual")) { + result.setTimex("P1Y"); + } + + if (result.getTimex() != "") { + result.setResult(true); + } + + return result; + } + + public MatchedTimexResult getMatchedUnitTimex(String text) { + + MatchedTimexResult result = new MatchedTimexResult(); + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + + if (trimmedText.equals("day")) { + result.setTimex("P1D"); + } else if (trimmedText.equals("week")) { + result.setTimex("P1W"); + } else if (trimmedText.equals("month")) { + result.setTimex("P1M"); + } else if (trimmedText.equals("year")) { + result.setTimex("P1Y"); + } + + if (result.getTimex() != "") { + result.setResult(true); + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishTimeParserConfiguration.java new file mode 100644 index 000000000..72fab44ab --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishTimeParserConfiguration.java @@ -0,0 +1,187 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.PrefixAdjustResult; +import com.microsoft.recognizers.text.datetime.parsers.config.SuffixAdjustResult; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public class EnglishTimeParserConfiguration extends BaseOptionsConfiguration implements ITimeParserConfiguration { + + private final ImmutableMap numbers; + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final IDateTimeParser timeZoneParser; + + private final Pattern atRegex; + private final Iterable timeRegexes; + private final Pattern timeSuffixFull = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeSuffixFull); + private final Pattern lunchRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LunchRegex); + private final Pattern nightRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.NightRegex); + + public EnglishTimeParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + timeZoneParser = new BaseTimeZoneParser(); + + atRegex = EnglishTimeExtractorConfiguration.AtRegex; + timeRegexes = EnglishTimeExtractorConfiguration.TimeRegexList; + } + + @Override + public String getTimeTokenPrefix() { + return EnglishDateTime.TimeTokenPrefix; + } + + @Override + public Pattern getAtRegex() { + return atRegex; + } + + @Override + public Iterable getTimeRegexes() { + return timeRegexes; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public PrefixAdjustResult adjustByPrefix(String prefix, int hour, int min, boolean hasMin) { + + int deltaMin; + String trimmedPrefix = prefix.trim().toLowerCase(); + + if (trimmedPrefix.startsWith("half")) { + deltaMin = 30; + } else if (trimmedPrefix.startsWith("a quarter") || trimmedPrefix.startsWith("quarter")) { + deltaMin = 15; + } else if (trimmedPrefix.startsWith("three quarter")) { + deltaMin = 45; + } else { + + Optional match = Arrays.stream(RegExpUtility.getMatches(EnglishTimeExtractorConfiguration.LessThanOneHour, trimmedPrefix)).findFirst(); + String minStr = match.get().getGroup("deltamin").value; + if (!StringUtility.isNullOrWhiteSpace(minStr)) { + deltaMin = Integer.parseInt(minStr); + } else { + minStr = match.get().getGroup("deltaminnum").value; + deltaMin = numbers.getOrDefault(minStr, 0); + } + } + + if (trimmedPrefix.endsWith("to")) { + deltaMin = -deltaMin; + } + + min += deltaMin; + if (min < 0) { + min += 60; + hour -= 1; + } + + hasMin = true; + + return new PrefixAdjustResult(hour, min, hasMin); + } + + @Override + public SuffixAdjustResult adjustBySuffix(String suffix, int hour, int min, boolean hasMin, boolean hasAm, boolean hasPm) { + + String lowerSuffix = suffix.toLowerCase(); + int deltaHour = 0; + ConditionalMatch match = RegexExtension.matchExact(timeSuffixFull, lowerSuffix, true); + if (match.getSuccess()) { + + String oclockStr = match.getMatch().get().getGroup("oclock").value; + if (StringUtility.isNullOrEmpty(oclockStr)) { + + String amStr = match.getMatch().get().getGroup(Constants.AmGroupName).value; + if (!StringUtility.isNullOrEmpty(amStr)) { + if (hour >= Constants.HalfDayHourCount) { + deltaHour = -Constants.HalfDayHourCount; + } else { + hasAm = true; + } + + } + + String pmStr = match.getMatch().get().getGroup(Constants.PmGroupName).value; + if (!StringUtility.isNullOrEmpty(pmStr)) { + if (hour < Constants.HalfDayHourCount) { + deltaHour = Constants.HalfDayHourCount; + } + + if (checkMatch(lunchRegex, pmStr)) { + // for hour >= 10, < 12 + if (hour >= 10 && hour <= Constants.HalfDayHourCount) { + deltaHour = 0; + if (hour == Constants.HalfDayHourCount) { + hasPm = true; + } else { + hasAm = true; + } + + } else { + hasPm = true; + } + + } else if (checkMatch(nightRegex, pmStr)) { + //For hour <= 3 or == 12, we treat it as am, for example 1 in the night (midnight) == 1am + if (hour <= 3 || hour == Constants.HalfDayHourCount) { + if (hour == Constants.HalfDayHourCount) { + hour = 0; + } + + deltaHour = 0; + hasAm = true; + } else { + hasPm = true; + } + + } else { + hasPm = true; + } + } + } + } + + hour = (hour + deltaHour) % 24; + + return new SuffixAdjustResult(hour, min, hasMin, hasAm, hasPm); + } + + private boolean checkMatch(Pattern regex, String input) { + return RegExpUtility.getMatches(regex, input).length > 0; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishTimePeriodParserConfiguration.java new file mode 100644 index 000000000..0d6d17779 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/EnglishTimePeriodParserConfiguration.java @@ -0,0 +1,159 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.TimeOfDayResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; + +import java.util.regex.Pattern; + +public class EnglishTimePeriodParserConfiguration extends BaseOptionsConfiguration implements ITimePeriodParserConfiguration { + + private final IDateTimeExtractor timeExtractor; + private final IDateTimeParser timeParser; + private final IExtractor integerExtractor; + private final IDateTimeParser timeZoneParser; + + private final Pattern specificTimeFromToRegex; + private final Pattern specificTimeBetweenAndRegex; + private final Pattern pureNumberFromToRegex; + private final Pattern pureNumberBetweenAndRegex; + private final Pattern timeOfDayRegex; + private final Pattern generalEndingRegex; + private final Pattern tillRegex; + + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final ImmutableMap numbers; + + public EnglishTimePeriodParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + timeExtractor = config.getTimeExtractor(); + integerExtractor = config.getIntegerExtractor(); + timeParser = config.getTimeParser(); + timeZoneParser = config.getTimeZoneParser(); + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + + pureNumberFromToRegex = EnglishTimePeriodExtractorConfiguration.PureNumFromTo; + pureNumberBetweenAndRegex = EnglishTimePeriodExtractorConfiguration.PureNumBetweenAnd; + specificTimeFromToRegex = EnglishTimePeriodExtractorConfiguration.SpecificTimeFromTo; + specificTimeBetweenAndRegex = EnglishTimePeriodExtractorConfiguration.SpecificTimeBetweenAnd; + timeOfDayRegex = EnglishTimePeriodExtractorConfiguration.TimeOfDayRegex; + + generalEndingRegex = EnglishTimePeriodExtractorConfiguration.GeneralEndingRegex; + tillRegex = EnglishTimePeriodExtractorConfiguration.TillRegex; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public Pattern getPureNumberFromToRegex() { + return pureNumberFromToRegex; + } + + @Override + public Pattern getPureNumberBetweenAndRegex() { + return pureNumberBetweenAndRegex; + } + + @Override + public Pattern getSpecificTimeFromToRegex() { + return specificTimeFromToRegex; + } + + @Override + public Pattern getSpecificTimeBetweenAndRegex() { + return specificTimeBetweenAndRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return timeOfDayRegex; + } + + @Override + public Pattern getGeneralEndingRegex() { + return generalEndingRegex; + } + + @Override + public Pattern getTillRegex() { + return tillRegex; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public MatchedTimeRangeResult getMatchedTimexRange(String text, String timex, int beginHour, int endHour, int endMin) { + + String trimmedText = text.trim().toLowerCase(); + if (trimmedText.endsWith("s")) { + trimmedText = trimmedText.substring(0, trimmedText.length() - 1); + } + + beginHour = 0; + endHour = 0; + endMin = 0; + + String timeOfDay = ""; + + if (EnglishDateTime.MorningTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Morning; + } else if (EnglishDateTime.AfternoonTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Afternoon; + } else if (EnglishDateTime.EveningTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Evening; + } else if (EnglishDateTime.DaytimeTermList.stream().anyMatch(trimmedText::equals)) { + timeOfDay = Constants.Daytime; + } else if (EnglishDateTime.NightTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Night; + } else if (EnglishDateTime.BusinessHourSplitStrings.stream().allMatch(trimmedText::contains)) { + timeOfDay = Constants.BusinessHour; + } else { + timex = null; + return new MatchedTimeRangeResult(false, timex, beginHour, endHour, endMin); + } + + TimeOfDayResolutionResult result = TimexUtility.parseTimeOfDay(timeOfDay); + + return new MatchedTimeRangeResult(true, result.getTimex(), result.getBeginHour(), result.getEndHour(), result.getEndMin()); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/TimeParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/TimeParser.java new file mode 100644 index 000000000..5c898aecc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/parsers/TimeParser.java @@ -0,0 +1,63 @@ +package com.microsoft.recognizers.text.datetime.english.parsers; + +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.english.extractors.EnglishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Optional; + +public class TimeParser extends BaseTimeParser { + + public TimeParser(ITimeParserConfiguration config) { + super(config); + } + + @Override + protected DateTimeResolutionResult internalParse(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult innerResult = super.internalParse(text, referenceTime); + + if (!innerResult.getSuccess()) { + innerResult = parseIsh(text, referenceTime); + } + + return innerResult; + } + + // parse "noonish", "11-ish" + private DateTimeResolutionResult parseIsh(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + String lowerText = text.toLowerCase(); + + ConditionalMatch match = RegexExtension.matchExact(EnglishTimeExtractorConfiguration.IshRegex, text, true); + if (match.getSuccess()) { + String hourStr = match.getMatch().get().getGroup(Constants.HourGroupName).value; + int hour = Constants.HalfDayHourCount; + + if (!StringUtility.isNullOrEmpty(hourStr)) { + hour = Integer.parseInt(hourStr); + } + + result.setTimex(String.format("T%02d", hour)); + LocalDateTime resultTime = DateUtil.safeCreateFromMinValue( + referenceTime.getYear(), + referenceTime.getMonthValue(), + referenceTime.getDayOfMonth(), + hour, 0, 0); + result.setFutureValue(resultTime); + result.setPastValue(resultTime); + result.setSuccess(true); + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/utilities/EnglishDatetimeUtilityConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/utilities/EnglishDatetimeUtilityConfiguration.java new file mode 100644 index 000000000..b85e88b72 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/english/utilities/EnglishDatetimeUtilityConfiguration.java @@ -0,0 +1,77 @@ +package com.microsoft.recognizers.text.datetime.english.utilities; + +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class EnglishDatetimeUtilityConfiguration implements IDateTimeUtilityConfiguration { + + public static final Pattern AgoRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AgoRegex); + public static final Pattern LaterRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.LaterRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.InConnectorRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.WithinNextPrefixRegex); + public static final Pattern AmDescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AmDescRegex); + public static final Pattern PmDescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.PmDescRegex); + public static final Pattern AmPmDescRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.AmPmDescRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.RangeUnitRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.TimeUnitRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.DateUnitRegex); + public static final Pattern CommonDatePrefixRegex = RegExpUtility.getSafeRegExp(EnglishDateTime.CommonDatePrefixRegex); + + @Override + public Pattern getAgoRegex() { + return AgoRegex; + } + + @Override + public Pattern getLaterRegex() { + return LaterRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getAmDescRegex() { + return AmDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return PmDescRegex; + } + + @Override + public Pattern getAmPmDescRegex() { + return AmPmDescRegex; + } + + @Override + public Pattern getCommonDatePrefixRegex() { + return CommonDatePrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/AbstractYearExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/AbstractYearExtractor.java new file mode 100644 index 000000000..65d13f941 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/AbstractYearExtractor.java @@ -0,0 +1,108 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateExtractorConfiguration; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.List; + +public abstract class AbstractYearExtractor implements IDateExtractor { + + protected final IDateExtractorConfiguration config; + + public AbstractYearExtractor(IDateExtractorConfiguration config) { + this.config = config; + } + + @Override + public abstract String getExtractorName(); + + @Override + public abstract List extract(String input, LocalDateTime reference); + + @Override + public abstract List extract(String input); + + @Override + public int getYearFromText(Match match) { + + int year = Constants.InvalidYear; + + String yearStr = match.getGroup("year").value; + String writtenYearStr = match.getGroup("fullyear").value; + + if (!StringUtility.isNullOrEmpty(yearStr) && !yearStr.equals(writtenYearStr)) { + + year = Math.round(Double.valueOf(yearStr).floatValue()); + + if (year < 100 && year >= Constants.MinTwoDigitYearPastNum) { + year += 1900; + } else if (year >= 0 && year < Constants.MaxTwoDigitYearFutureNum) { + year += 2000; + } + } else { + + MatchGroup firstTwoYear = match.getGroup("firsttwoyearnum"); + + if (!StringUtility.isNullOrEmpty(firstTwoYear.value)) { + ExtractResult er = new ExtractResult(); + er.setStart(firstTwoYear.index); + er.setLength(firstTwoYear.length); + er.setText(firstTwoYear.value); + + int firstTwoYearNum = Math.round(Double.valueOf((double)config.getNumberParser().parse(er).getValue()).floatValue()); + + int lastTwoYearNum = 0; + + MatchGroup lastTwoYear = match.getGroup("lasttwoyearnum"); + + if (!StringUtility.isNullOrEmpty(lastTwoYear.value)) { + er = new ExtractResult(); + er.setStart(lastTwoYear.index); + er.setLength(lastTwoYear.length); + er.setText(lastTwoYear.value); + + lastTwoYearNum = Math.round(Double.valueOf((double)config.getNumberParser().parse(er).getValue()).floatValue()); + } + + // Exclude pure number like "nineteen", "twenty four" + if (firstTwoYearNum < 100 && lastTwoYearNum == 0 || firstTwoYearNum < 100 && firstTwoYearNum % 10 == 0 && lastTwoYear.value.trim().split(" ").length == 1) { + year = Constants.InvalidYear; + return year; + } + + if (firstTwoYearNum >= 100) { + year = firstTwoYearNum + lastTwoYearNum; + } else { + year = firstTwoYearNum * 100 + lastTwoYearNum; + } + + } else { + + if (!StringUtility.isNullOrEmpty(writtenYearStr)) { + + MatchGroup writtenYear = match.getGroup("fullyear"); + + ExtractResult er = new ExtractResult(); + er.setStart(writtenYear.index); + er.setLength(writtenYear.length); + er.setText(writtenYear.value); + + year = Math.round(Double.valueOf((double)config.getNumberParser().parse(er).getValue()).floatValue()); + + if (year < 100 && year >= Constants.MinTwoDigitYearPastNum) { + year += 1900; + } else if (year >= 0 && year < Constants.MaxTwoDigitYearFutureNum) { + year += 2000; + } + } + } + } + + return year; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateExtractor.java new file mode 100644 index 000000000..532d7947f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateExtractor.java @@ -0,0 +1,497 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.AgoLaterUtil; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.javatuples.Pair; + +public class BaseDateExtractor extends AbstractYearExtractor implements IDateTimeExtractor { + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DATE; + } + + public BaseDateExtractor(IDateExtractorConfiguration config) { + super(config); + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + @Override + public List extract(String input, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + tokens.addAll(basicRegexMatch(input)); + tokens.addAll(implicitDate(input)); + tokens.addAll(numberWithMonth(input, reference)); + tokens.addAll(extractRelativeDurationDate(input, reference)); + + return Token.mergeAllTokens(tokens, input, getExtractorName()); + } + + // match basic patterns in DateRegexList + private Collection basicRegexMatch(String text) { + List result = new ArrayList<>(); + + for (Pattern regex : config.getDateRegexList()) { + Match[] matches = RegExpUtility.getMatches(regex, text); + + for (Match match : matches) { + // some match might be part of the date range entity, and might be splitted in a wrong way + + if (validateMatch(match, text)) { + // Cases that the relative term is before the detected date entity, like "this 5/12", "next friday 5/12" + String preText = text.substring(0, match.index); + ConditionalMatch relativeRegex = RegexExtension.matchEnd(config.getStrictRelativeRegex(), preText, true); + if (relativeRegex.getSuccess()) { + result.add(new Token(relativeRegex.getMatch().get().index, match.index + match.length)); + } else { + result.add(new Token(match.index, match.index + match.length)); + } + } + } + } + + return result; + } + + // this method is to validate whether the match is part of date range and is a correct split + // For example: in case "10-1 - 11-7", "10-1 - 11" can be matched by some of the Regexes, + // but the full text is a date range, so "10-1 - 11" is not a correct split + private boolean validateMatch(Match match, String text) { + // If the match doesn't contains "year" part, it will not be ambiguous and it's a valid match + boolean isValidMatch = StringUtility.isNullOrEmpty(match.getGroup("year").value); + + if (!isValidMatch) { + MatchGroup yearGroup = match.getGroup("year"); + + // If the "year" part is not at the end of the match, it's a valid match + if (yearGroup.index + yearGroup.length != match.index + match.length) { + isValidMatch = true; + } else { + String subText = text.substring(yearGroup.index); + + // If the following text (include the "year" part) doesn't start with a Date entity, it's a valid match + if (!startsWithBasicDate(subText)) { + isValidMatch = true; + } else { + // If the following text (include the "year" part) starts with a Date entity, + // but the following text (doesn't include the "year" part) also starts with a valid Date entity, + // the current match is still valid + // For example, "10-1-2018-10-2-2018". Match "10-1-2018" is valid because though "2018-10-2" a valid match + // (indicates the first year "2018" might belongs to the second Date entity), but "10-2-2018" is also a valid match. + subText = text.substring(yearGroup.index + yearGroup.length).trim(); + subText = trimStartRangeConnectorSymbols(subText); + isValidMatch = startsWithBasicDate(subText); + } + } + + // Expressions with mixed separators are not considered valid dates e.g. "30/4.85" (unless one is a comma "30/4, 2016") + MatchGroup dayGroup = match.getGroup("day"); + MatchGroup monthGroup = match.getGroup("month"); + if (!StringUtility.isNullOrEmpty(dayGroup.value) && !StringUtility.isNullOrEmpty(monthGroup.value)) { + String noDateText = match.value.replace(yearGroup.value, "") + .replace(monthGroup.value, "").replace(dayGroup.value, ""); + String[] separators = {"/", "\\", "-", "."}; + int separatorCount = 0; + for (String separator : separators) { + if (noDateText.contains(separator)) { + separatorCount++; + } + if (separatorCount > 1) { + isValidMatch = false; + break; + } + } + } + } + + return isValidMatch; + } + + // TODO: Simplify this method to improve the performance + private String trimStartRangeConnectorSymbols(String text) { + Match[] rangeConnectorSymbolMatches = RegExpUtility.getMatches(config.getRangeConnectorSymbolRegex(), text); + + for (Match symbolMatch : rangeConnectorSymbolMatches) { + int startSymbolLength = -1; + + if (symbolMatch.value != "" && symbolMatch.index == 0 && symbolMatch.length > startSymbolLength) { + startSymbolLength = symbolMatch.length; + } + + if (startSymbolLength > 0) { + text = text.substring(startSymbolLength); + } + } + + return text.trim(); + } + + // TODO: Simplify this method to improve the performance + private boolean startsWithBasicDate(String text) { + for (Pattern regex : config.getDateRegexList()) { + ConditionalMatch match = RegexExtension.matchBegin(regex, text, true); + + if (match.getSuccess()) { + return true; + } + } + + return false; + } + + // match several other cases + // including 'today', 'the day after tomorrow', 'on 13' + private Collection implicitDate(String text) { + List result = new ArrayList<>(); + + for (Pattern regex : config.getImplicitDateList()) { + Match[] matches = RegExpUtility.getMatches(regex, text); + + for (Match match : matches) { + result.add(new Token(match.index, match.index + match.length)); + } + } + + return result; + } + + // Check every integers and ordinal number for date + private Collection numberWithMonth(String text, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + List ers = config.getOrdinalExtractor().extract(text); + ers.addAll(config.getIntegerExtractor().extract(text)); + + for (ExtractResult result : ers) { + int num; + try { + ParseResult parseResult = config.getNumberParser().parse(result); + num = Float.valueOf(parseResult.getValue().toString()).intValue(); + } catch (NumberFormatException e) { + num = 0; + } + + if (num < 1 || num > 31) { + continue; + } + + if (result.getStart() >= 0) { + // Handling cases like '(Monday,) Jan twenty two' + String frontStr = text.substring(0, result.getStart()); + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getMonthEnd(), frontStr)).findFirst(); + if (match.isPresent()) { + int startIndex = match.get().index; + int endIndex = match.get().index + match.get().length + result.getLength(); + + int month = config.getMonthOfYear().getOrDefault(match.get().getGroup("month").value.toLowerCase(), reference.getMonthValue()); + + Pair startEnd = extendWithWeekdayAndYear(startIndex, endIndex, month, num, text, reference); + + tokens.add(new Token(startEnd.getValue0(), startEnd.getValue1())); + continue; + } + + // Handling cases like 'for the 25th' + Match[] matches = RegExpUtility.getMatches(config.getForTheRegex(), text); + boolean isFound = false; + + for (Match matchCase : matches) { + if (matchCase != null) { + String ordinalNum = matchCase.getGroup("DayOfMonth").value; + if (ordinalNum.equals(result.getText())) { + int endLenght = 0; + if (!matchCase.getGroup("end").value.equals("")) { + endLenght = matchCase.getGroup("end").value.length(); + } + + tokens.add(new Token(matchCase.index, matchCase.index + matchCase.length - endLenght)); + isFound = true; + } + } + } + + if (isFound) { + continue; + } + + // Handling cases like 'Thursday the 21st', which both 'Thursday' and '21st' refer to a same date + matches = RegExpUtility.getMatches(config.getWeekDayAndDayOfMonthRegex(), text); + isFound = false; + for (Match matchCase : matches) { + if (matchCase != null) { + String ordinalNum = matchCase.getGroup("DayOfMonth").value; + if (ordinalNum.equals(result.getText())) { + // Get week of day for the ordinal number which is regarded as a date of reference month + LocalDateTime date = DateUtil.safeCreateFromMinValue(reference.getYear(), reference.getMonthValue(), num); + String numWeekDayStr = date.getDayOfWeek().toString().toLowerCase(); + + // Get week day from text directly, compare it with the weekday generated above + // to see whether they refer to the same week day + String extractedWeekDayStr = matchCase.getGroup("weekday").value.toLowerCase(); + int numWeekDay = config.getDayOfWeek().get(numWeekDayStr); + int extractedWeekDay = config.getDayOfWeek().get(extractedWeekDayStr); + + if (date != DateUtil.minValue() && numWeekDay == extractedWeekDay) { + tokens.add(new Token(matchCase.index, result.getStart() + result.getLength())); + isFound = true; + } + } + } + } + + if (isFound) { + continue; + } + + // Handling cases like '20th of next month' + String suffixStr = text.substring(result.getStart() + result.getLength()); + ConditionalMatch beginMatch = RegexExtension.matchBegin(config.getRelativeMonthRegex(), suffixStr.trim(), true); + if (beginMatch.getSuccess() && beginMatch.getMatch().get().index == 0) { + int spaceLen = suffixStr.length() - suffixStr.trim().length(); + int resStart = result.getStart(); + int resEnd = resStart + result.getLength() + spaceLen + beginMatch.getMatch().get().length; + + // Check if prefix contains 'the', include it if any + String prefix = text.substring(0, resStart); + Optional prefixMatch = Arrays.stream(RegExpUtility.getMatches(config.getPrefixArticleRegex(), prefix)).findFirst(); + if (prefixMatch.isPresent()) { + resStart = prefixMatch.get().index; + } + + tokens.add(new Token(resStart, resEnd)); + } + + // Handling cases like 'second Sunday' + suffixStr = text.substring(result.getStart() + result.getLength()); + beginMatch = RegexExtension.matchBegin(config.getWeekDayRegex(), suffixStr.trim(), true); + if (beginMatch.getSuccess() && num >= 1 && num <= 5 && result.getType().equals("builtin.num.ordinal")) { + String weekDayStr = beginMatch.getMatch().get().getGroup("weekday").value.toLowerCase(); + if (config.getDayOfWeek().containsKey(weekDayStr)) { + int spaceLen = suffixStr.length() - suffixStr.trim().length(); + tokens.add(new Token(result.getStart(), result.getStart() + result.getLength() + spaceLen + beginMatch.getMatch().get().length)); + } + } + } + + // For cases like "I'll go back twenty second of June" + if (result.getStart() + result.getLength() < text.length()) { + String afterStr = text.substring(result.getStart() + result.getLength()); + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getOfMonth(), afterStr)).findFirst(); + if (match.isPresent()) { + int startIndex = result.getStart(); + int endIndex = result.getStart() + result.getLength() + match.get().length; + + int month = config.getMonthOfYear().getOrDefault(match.get().getGroup("month").value.toLowerCase(), reference.getMonthValue()); + + Pair startEnd = extendWithWeekdayAndYear(startIndex, endIndex, month, num, text, reference); + tokens.add(new Token(startEnd.getValue0(), startEnd.getValue1())); + } + } + } + + return tokens; + } + + private Pair extendWithWeekdayAndYear(int startIndex, int endIndex, int month, int day, String text, LocalDateTime reference) { + int year = reference.getYear(); + int startIndexResult = startIndex; + int endIndexResult = endIndex; + + // Check whether there's a year + String suffix = text.substring(endIndexResult); + Optional matchYear = Arrays.stream(RegExpUtility.getMatches(config.getYearSuffix(), suffix)).findFirst(); + + if (matchYear.isPresent() && matchYear.get().index == 0) { + year = getYearFromText(matchYear.get()); + + if (year >= Constants.MinYearNum && year <= Constants.MaxYearNum) { + endIndexResult += matchYear.get().length; + } + } + + LocalDateTime date = DateUtil.safeCreateFromMinValue(year, month, day); + + // Check whether there's a weekday + String prefix = text.substring(0, startIndexResult); + Optional matchWeekDay = Arrays.stream(RegExpUtility.getMatches(config.getWeekDayEnd(), prefix)).findFirst(); + if (matchWeekDay.isPresent()) { + // Get weekday from context directly, compare it with the weekday extraction above + // to see whether they are referred to the same weekday + String extractedWeekDayStr = matchWeekDay.get().getGroup("weekday").value.toLowerCase(); + String numWeekDayStr = date.getDayOfWeek().toString().toLowerCase(); + + if (config.getDayOfWeek().containsKey(numWeekDayStr) && config.getDayOfWeek().containsKey(extractedWeekDayStr)) { + int weekDay1 = config.getDayOfWeek().get(numWeekDayStr); + int weekday2 = config.getDayOfWeek().get(extractedWeekDayStr); + if (date != DateUtil.minValue() && weekDay1 == weekday2) { + startIndexResult = matchWeekDay.get().index; + } + + } + } + + return new Pair<>(startIndexResult, endIndexResult); + } + + // Cases like "3 days from today", "5 weeks before yesterday", "2 months after tomorrow" + // Note that these cases are of type "date" + private Collection extractRelativeDurationDate(String text, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + List durations = config.getDurationExtractor().extract(text, reference); + + for (ExtractResult duration : durations) { + // if it is a multiple duration but its type is not equal to Date, skip it here + if (isMultipleDuration(duration) && !isMultipleDurationDate(duration)) { + continue; + } + + // Some types of duration can be compounded with "before", "after" or "from" suffix to create a "date" + // While some other types of durations, when compounded with such suffix, it will not create a "date", but create a "dateperiod" + // For example, durations like "3 days", "2 weeks", "1 week and 2 days", can be compounded with such suffix to create a "date" + // But "more than 3 days", "less than 2 weeks", when compounded with such suffix, it will become cases + // like "more than 3 days from today" which is a "dateperiod", not a "date" + // As this parent method is aimed to extract RelativeDurationDate, so for cases with "more than" or "less than", + // we remove the prefix so as to extract the expected RelativeDurationDate + if (isInequalityDuration(duration)) { + duration = stripInequalityDuration(duration); + } + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getDateUnitRegex(), duration.getText())).findFirst(); + + if (match.isPresent()) { + tokens = AgoLaterUtil.extractorDurationWithBeforeAndAfter(text, duration, tokens, config.getUtilityConfiguration()); + } + + } + + // Extract cases like "in 3 weeks", which equals to "3 weeks from today" + List relativeDurationDateWithInPrefix = extractRelativeDurationDateWithInPrefix(text, durations, reference); + + // For cases like "in 3 weeks from today", we should choose "3 weeks from today" as the extract result rather than "in 3 weeks" or "in 3 weeks from today" + for (Token erWithInPrefix : relativeDurationDateWithInPrefix) { + if (!isOverlapWithExistExtractions(erWithInPrefix, tokens)) { + tokens.add(erWithInPrefix); + } + } + + return tokens; + } + + public boolean isOverlapWithExistExtractions(Token er, List existErs) { + for (Token existEr : existErs) { + if (er.getStart() < existEr.getEnd() && er.getEnd() > existEr.getStart()) { + return true; + } + } + + return false; + } + + // "In 3 days/weeks/months/years" = "3 days/weeks/months/years from now" + public List extractRelativeDurationDateWithInPrefix(String text, List durationEr, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + List durations = new ArrayList<>(); + + for (ExtractResult durationExtraction : durationEr) { + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getDateUnitRegex(), durationExtraction.getText())).findFirst(); + if (match.isPresent()) { + int start = durationExtraction.getStart() != null ? durationExtraction.getStart() : 0; + int end = start + (durationExtraction.getLength() != null ? durationExtraction.getLength() : 0); + durations.add(new Token(start, end)); + } + } + + for (Token duration : durations) { + String beforeStr = text.substring(0, duration.getStart()).toLowerCase(); + String afterStr = text.substring(duration.getStart() + duration.getLength()).toLowerCase(); + + if (StringUtility.isNullOrWhiteSpace(beforeStr) && StringUtility.isNullOrWhiteSpace(afterStr)) { + continue; + } + + ConditionalMatch match = RegexExtension.matchEnd(config.getInConnectorRegex(), beforeStr, true); + + if (match.getSuccess() && match.getMatch().isPresent()) { + int startToken = match.getMatch().get().index; + Optional rangeUnitMatch = Arrays.stream( + RegExpUtility.getMatches(config.getRangeUnitRegex(), + text.substring(duration.getStart(), + duration.getStart() + duration.getLength()))).findFirst(); + + if (rangeUnitMatch.isPresent()) { + tokens.add(new Token(startToken, duration.getEnd())); + } + } + } + + return tokens; + } + + private ExtractResult stripInequalityDuration(ExtractResult er) { + ExtractResult result = er; + result = stripInequalityPrefix(result, config.getMoreThanRegex()); + result = stripInequalityPrefix(result, config.getLessThanRegex()); + return result; + } + + private ExtractResult stripInequalityPrefix(ExtractResult er, Pattern regex) { + ExtractResult result = er; + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, er.getText())).findFirst(); + + if (match.isPresent()) { + int originalLength = er.getText().length(); + String text = er.getText().replace(match.get().value, "").trim(); + int start = er.getStart() + originalLength - text.length(); + int length = text.length(); + String data = ""; + result.setStart(start); + result.setLength(length); + result.setText(text); + result.setData(data); + } + + return result; + } + + // Cases like "more than 3 days", "less than 4 weeks" + private boolean isInequalityDuration(ExtractResult er) { + return er.getData() != null && (er.getData().toString().equals(Constants.MORE_THAN_MOD) || er.getData().toString().equals(Constants.LESS_THAN_MOD)); + } + + private boolean isMultipleDurationDate(ExtractResult er) { + return er.getData() != null && er.getData().toString().equals(Constants.MultipleDuration_Date); + } + + private boolean isMultipleDuration(ExtractResult er) { + return er.getData() != null && er.getData().toString().startsWith(Constants.MultipleDuration_Prefix); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDatePeriodExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDatePeriodExtractor.java new file mode 100644 index 000000000..19d792327 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDatePeriodExtractor.java @@ -0,0 +1,464 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.Metadata; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.IDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class BaseDatePeriodExtractor implements IDateTimeExtractor { + + private final IDatePeriodExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DATEPERIOD; + } + + public BaseDatePeriodExtractor(IDatePeriodExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + @Override + public List extract(String input, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + tokens.addAll(matchSimpleCases(input)); + + List simpleCasesResults = Token.mergeAllTokens(tokens, input, getExtractorName()); + List ordinalExtractions = config.getOrdinalExtractor().extract(input); + + tokens.addAll(mergeTwoTimePoints(input, reference)); + tokens.addAll(matchDuration(input, reference)); + tokens.addAll(singleTimePointWithPatterns(input, ordinalExtractions, reference)); + tokens.addAll(matchComplexCases(input, simpleCasesResults, reference)); + tokens.addAll(matchYearPeriod(input, reference)); + tokens.addAll(matchOrdinalNumberWithCenturySuffix(input, ordinalExtractions)); + + return Token.mergeAllTokens(tokens, input, getExtractorName()); + } + + private List matchSimpleCases(String input) { + List results = new ArrayList<>(); + + for (Pattern regex : config.getSimpleCasesRegexes()) { + Match[] matches = RegExpUtility.getMatches(regex, input); + + for (Match match : matches) { + Optional matchYear = Arrays.stream(RegExpUtility.getMatches(config.getYearRegex(), match.value)).findFirst(); + + if (matchYear.isPresent() && matchYear.get().length == match.length) { + int year = ((BaseDateExtractor)config.getDatePointExtractor()).getYearFromText(matchYear.get()); + if (!(year >= Constants.MinYearNum && year <= Constants.MaxYearNum)) { + continue; + } + } + + // handle single year which is surrounded by '-' at both sides, e.g., a single year falls in a GUID + if (match.length == Constants.FourDigitsYearLength && + RegExpUtility.getMatches(this.config.getYearRegex(), match.value).length > 0 && + infixBoundaryCheck(match, input)) { + String subStr = input.substring(match.index - 1, match.index - 1 + 6); + if (RegExpUtility.getMatches(this.config.getIllegalYearRegex(), subStr).length > 0) { + continue; + } + } + + results.add(new Token(match.index, match.index + match.length)); + } + + } + + return results; + } + + private List mergeTwoTimePoints(String input, LocalDateTime reference) { + List ers = config.getDatePointExtractor().extract(input, reference); + + // Handle "now" + Match[] matches = RegExpUtility.getMatches(this.config.getNowRegex(), input); + if (matches.length != 0) { + for (Match match : matches) { + ers.add(new ExtractResult(match.index, match.length, match.value, Constants.SYS_DATETIME_DATE)); + } + + ers.sort(Comparator.comparingInt(arg -> arg.getStart())); + } + + return mergeMultipleExtractions(input, ers); + } + + private List mergeMultipleExtractions(String input, List extractionResults) { + List results = new ArrayList<>(); + + Metadata metadata = new Metadata() { + { + setPossiblyIncludePeriodEnd(true); + } + }; + + if (extractionResults.size() <= 1) { + return results; + } + + int idx = 0; + + while (idx < extractionResults.size() - 1) { + ExtractResult thisResult = extractionResults.get(idx); + ExtractResult nextResult = extractionResults.get(idx + 1); + + int middleBegin = thisResult.getStart() + thisResult.getLength(); + int middleEnd = nextResult.getStart(); + if (middleBegin >= middleEnd) { + idx++; + continue; + } + + String middleStr = input.substring(middleBegin, middleEnd).trim().toLowerCase(); + + if (RegexExtension.isExactMatch(config.getTillRegex(), middleStr, true)) { + int periodBegin = thisResult.getStart(); + int periodEnd = nextResult.getStart() + nextResult.getLength(); + + // handle "from/between" together with till words (till/until/through...) + String beforeStr = input.substring(0, periodBegin).trim().toLowerCase(); + + ResultIndex fromIndex = config.getFromTokenIndex(beforeStr); + ResultIndex betweenIndex = config.getBetweenTokenIndex(beforeStr); + + if (fromIndex.getResult()) { + periodBegin = fromIndex.getIndex(); + } else if (betweenIndex.getResult()) { + periodBegin = betweenIndex.getIndex(); + } + + results.add(new Token(periodBegin, periodEnd, metadata)); + + // merge two tokens here, increase the index by two + idx += 2; + continue; + } + + boolean hasConnectorToken = config.hasConnectorToken(middleStr); + if (hasConnectorToken) { + int periodBegin = thisResult.getStart(); + int periodEnd = nextResult.getStart() + nextResult.getLength(); + + // handle "between...and..." case + String beforeStr = input.substring(0, periodBegin).trim().toLowerCase(); + + ResultIndex beforeIndex = config.getBetweenTokenIndex(beforeStr); + + if (beforeIndex.getResult()) { + periodBegin = beforeIndex.getIndex(); + results.add(new Token(periodBegin, periodEnd, metadata)); + + // merge two tokens here, increase the index by two + idx += 2; + continue; + } + } + idx++; + } + + return results; + } + + private List matchDuration(String input, LocalDateTime reference) { + + List results = new ArrayList<>(); + + List durations = new ArrayList<>(); + Iterable durationExtractions = config.getDurationExtractor().extract(input, reference); + + for (ExtractResult durationExtraction : durationExtractions) { + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getDateUnitRegex(), durationExtraction.getText())).findFirst(); + if (match.isPresent()) { + durations.add(new Token(durationExtraction.getStart(), durationExtraction.getStart() + durationExtraction.getLength())); + } + } + + for (Token duration : durations) { + String beforeStr = input.substring(0, duration.getStart()).toLowerCase(); + String afterStr = input.substring(duration.getStart() + duration.getLength()).toLowerCase(); + + if (StringUtility.isNullOrWhiteSpace(beforeStr) && StringUtility.isNullOrWhiteSpace(afterStr)) { + continue; + } + + // within "Days/Weeks/Months/Years" should be handled as dateRange here + // if duration contains "Seconds/Minutes/Hours", it should be treated as datetimeRange + ConditionalMatch match = RegexExtension.matchEnd(config.getWithinNextPrefixRegex(), beforeStr, true); + + if (match.getSuccess()) { + int startToken = match.getMatch().get().index; + String tokenString = input.substring(duration.getStart(), duration.getEnd()); + Match matchDate = Arrays.stream(RegExpUtility.getMatches(config.getDateUnitRegex(), tokenString)).findFirst().orElse(null); + Match matchTime = Arrays.stream(RegExpUtility.getMatches(config.getTimeUnitRegex(), tokenString)).findFirst().orElse(null); + + if (matchDate != null && matchTime == null) { + results.add(new Token(startToken, duration.getEnd())); + continue; + } + } + + // Match prefix + match = RegexExtension.matchEnd(config.getPastRegex(), beforeStr, true); + + int index = -1; + + if (match.getSuccess()) { + index = match.getMatch().get().index; + } + + if (index < 0) { + // For cases like "next five days" + match = RegexExtension.matchEnd(config.getFutureRegex(), beforeStr, true); + + if (match.getSuccess()) { + index = match.getMatch().get().index; + } + } + + if (index >= 0) { + String prefix = beforeStr.substring(0, index).trim(); + String durationText = input.substring(duration.getStart(), duration.getStart() + duration.getLength()); + List numbersInPrefix = config.getCardinalExtractor().extract(prefix); + List numbersInDuration = config.getCardinalExtractor().extract(durationText); + + // Cases like "2 upcoming days", should be supported here + // Cases like "2 upcoming 3 days" is invalid, only extract "upcoming 3 days" by default + if (!numbersInPrefix.isEmpty() && numbersInDuration.isEmpty()) { + ExtractResult lastNumber = numbersInPrefix.stream() + .sorted(Comparator.comparingInt(x -> x.getStart() + x.getLength())) + .reduce((acc, item) -> item).orElse(null); + + // Prefix should ends with the last number + if (lastNumber.getStart() + lastNumber.getLength() == prefix.length()) { + results.add(new Token(lastNumber.getStart(), duration.getEnd())); + } + + } else { + results.add(new Token(index, duration.getEnd())); + } + + continue; + } + + // Match suffix + match = RegexExtension.matchBegin(config.getPastRegex(), afterStr, true); + if (match.getSuccess()) { + int matchLength = match.getMatch().get().index + match.getMatch().get().length; + results.add(new Token(duration.getStart(), duration.getEnd() + matchLength)); + continue; + } + + match = RegexExtension.matchBegin(config.getFutureSuffixRegex(), afterStr, true); + if (match.getSuccess()) { + int matchLength = match.getMatch().get().index + match.getMatch().get().length; + results.add(new Token(duration.getStart(), duration.getEnd() + matchLength)); + } + } + + return results; + } + + // 1. Extract the month of date, week of date to a date range + // 2. Extract cases like within two weeks from/before today/tomorrow/yesterday + private List singleTimePointWithPatterns(String input, List ordinalExtractions, LocalDateTime reference) { + List results = new ArrayList<>(); + + List datePoints = config.getDatePointExtractor().extract(input, reference); + + // For cases like "week of the 18th" + datePoints.addAll(ordinalExtractions.stream().filter(o -> datePoints.stream().noneMatch(er -> er.isOverlap(o))).collect(Collectors.toList())); + + if (datePoints.size() < 1) { + return results; + } + + for (ExtractResult er : datePoints) { + if (er.getStart() != null && er.getLength() != null) { + String beforeStr = input.substring(0, er.getStart()); + results.addAll(getTokenForRegexMatching(beforeStr, config.getWeekOfRegex(), er)); + results.addAll(getTokenForRegexMatching(beforeStr, config.getMonthOfRegex(), er)); + + // Cases like "3 days from today", "2 weeks before yesterday", "3 months after tomorrow" + if (isRelativeDurationDate(er)) { + results.addAll(getTokenForRegexMatching(beforeStr, config.getLessThanRegex(), er)); + results.addAll(getTokenForRegexMatching(beforeStr, config.getMoreThanRegex(), er)); + + // For "within" case, only duration with relative to "today" or "now" makes sense + // Cases like "within 3 days from yesterday/tomorrow" does not make any sense + if (isDateRelativeToNowOrToday(er)) { + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getWithinNextPrefixRegex(), beforeStr)).findFirst(); + if (match.isPresent()) { + boolean isNext = !StringUtility.isNullOrEmpty(match.get().getGroup(Constants.NextGroupName).value); + + // For "within" case + // Cases like "within the next 5 days before today" is not acceptable + if (!(isNext && isAgoRelativeDurationDate(er))) { + results.addAll(getTokenForRegexMatching(beforeStr, config.getWithinNextPrefixRegex(), er)); + } + } + } + } + } + } + + return results; + } + + private boolean isAgoRelativeDurationDate(ExtractResult er) { + return Arrays.stream(RegExpUtility.getMatches(config.getAgoRegex(), er.getText())).findAny().isPresent(); + } + + // Cases like "3 days from today", "2 weeks before yesterday", "3 months after + // tomorrow" + private boolean isRelativeDurationDate(ExtractResult er) { + boolean isAgo = Arrays.stream(RegExpUtility.getMatches(config.getAgoRegex(), er.getText())).findAny().isPresent(); + boolean isLater = Arrays.stream(RegExpUtility.getMatches(config.getLaterRegex(), er.getText())).findAny().isPresent(); + + return isAgo || isLater; + } + + private List getTokenForRegexMatching(String source, Pattern regex, ExtractResult er) { + List results = new ArrayList<>(); + Match match = Arrays.stream(RegExpUtility.getMatches(regex, source)).findFirst().orElse(null); + if (match != null && source.trim().endsWith(match.value.trim())) { + int startIndex = source.lastIndexOf(match.value); + results.add(new Token(startIndex, er.getStart() + er.getLength())); + } + + return results; + } + + // Complex cases refer to the combination of daterange and datepoint + // For Example: from|between {DateRange|DatePoint} to|till|and {DateRange|DatePoint} + private List matchComplexCases(String input, List simpleCasesResults, LocalDateTime reference) { + List ers = config.getDatePointExtractor().extract(input, reference); + + // Filter out DateRange results that are part of DatePoint results + // For example, "Feb 1st 2018" => "Feb" and "2018" should be filtered out here + List simpleErs = simpleCasesResults.stream().filter(simpleDateRange -> filterErs(simpleDateRange, ers)).collect(Collectors.toList()); + ers.addAll(simpleErs); + + List results = ers.stream().sorted((o1, o2) -> o1.getStart().compareTo(o2.getStart())).collect(Collectors.toList()); + + return mergeMultipleExtractions(input, results); + } + + private boolean filterErs(ExtractResult simpleDateRange, List ers) { + return !ers.stream().anyMatch(datePoint -> compareErs(simpleDateRange, datePoint)); + } + + private boolean compareErs(ExtractResult simpleDateRange, ExtractResult datePoint) { + return datePoint.getStart() <= simpleDateRange.getStart() && datePoint.getStart() + datePoint.getLength() >= simpleDateRange.getStart() + simpleDateRange.getLength(); + } + + private List matchYearPeriod(String input, LocalDateTime reference) { + List results = new ArrayList<>(); + Metadata metadata = new Metadata() { + { + setPossiblyIncludePeriodEnd(true); + } + }; + + Match[] matches = RegExpUtility.getMatches(config.getYearPeriodRegex(), input); + for (Match match : matches) { + Match matchYear = Arrays.stream(RegExpUtility.getMatches(config.getYearRegex(), match.value)).findFirst().orElse(null); + if (matchYear != null && matchYear.length == match.value.length()) { + int year = ((BaseDateExtractor)config.getDatePointExtractor()).getYearFromText(matchYear); + if (!(year >= Constants.MinYearNum && year <= Constants.MaxYearNum)) { + continue; + } + // Possibly include period end only apply for cases like "2014-2018", which are not single year cases + metadata.setPossiblyIncludePeriodEnd(false); + } else { + Match[] yearMatches = RegExpUtility.getMatches(config.getYearRegex(), match.value); + boolean isValidYear = true; + for (Match yearMatch : yearMatches) { + int year = ((BaseDateExtractor)config.getDatePointExtractor()).getYearFromText(yearMatch); + if (!(year >= Constants.MinYearNum && year <= Constants.MaxYearNum)) { + isValidYear = false; + break; + } + } + + if (!isValidYear) { + continue; + } + + } + + results.add(new Token(match.index, match.index + match.length, metadata)); + } + + return results; + } + + private List matchOrdinalNumberWithCenturySuffix(String input, List ordinalExtractions) { + List results = new ArrayList<>(); + + for (ExtractResult er : ordinalExtractions) { + if (er.getStart() + er.getLength() >= input.length()) { + continue; + } + + String afterStr = input.substring(er.getStart() + er.getLength()); + String trimmedAfterStr = afterStr.trim(); + int whiteSpacesCount = afterStr.length() - trimmedAfterStr.length(); + int afterStringOffset = er.getStart() + er.getLength() + whiteSpacesCount; + + Match match = Arrays.stream(RegExpUtility.getMatches(config.getCenturySuffixRegex(), trimmedAfterStr)).findFirst().orElse(null); + + if (match != null) { + results.add(new Token(er.getStart(), afterStringOffset + match.index + match.length)); + } + } + + return results; + } + + private boolean isDateRelativeToNowOrToday(ExtractResult input) { + for (String flagWord : config.getDurationDateRestrictions()) { + if (input.getText().contains(flagWord)) { + return true; + } + } + + return false; + } + + // check whether the match is an infix of source + private boolean infixBoundaryCheck(Match match, String source) { + boolean isMatchInfixOfSource = false; + if (match.index > 0 && match.index + match.length < source.length()) { + if (source.substring(match.index, match.index + match.length).equals(match.value)) { + isMatchInfixOfSource = true; + } + } + + return isMatchInfixOfSource; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimeAltExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimeAltExtractor.java new file mode 100644 index 000000000..13362108b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimeAltExtractor.java @@ -0,0 +1,598 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtendedModelResult; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeAltExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class BaseDateTimeAltExtractor implements IDateTimeListExtractor { + + private final IDateTimeAltExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DATETIMEALT; + } + + public BaseDateTimeAltExtractor(IDateTimeAltExtractorConfiguration config) { + this.config = config; + } + + public List extract(List extractResults, String text) { + return this.extract(extractResults, text, LocalDateTime.now()); + } + + @Override + public List extract(List extractResults, String text, LocalDateTime reference) { + return extractAlt(extractResults, text, reference); + } + + // Modify time entity to an alternative DateTime expression, such as "8pm" in "Monday 7pm or 8pm" + // or "Thursday" in "next week on Tuesday or Thursday" + private List extractAlt(List extractResults, String text, LocalDateTime reference) { + List ers = addImplicitDates(extractResults, text); + + // Sort the extracted results for the further sequential process. + ers.sort(Comparator.comparingInt(erA -> erA.getStart())); + + int i = 0; + while (i < ers.size() - 1) { + List altErs = getAltErsWithSameParentText(ers, i, text); + + if (altErs.size() == 0) { + i++; + continue; + } + + int j = i + altErs.size() - 1; + + int parentTextStart = ers.get(i).getStart(); + int parentTextLen = ers.get(j).getStart() + ers.get(j).getLength() - ers.get(i).getStart(); + String parentText = text.substring(parentTextStart, parentTextStart + parentTextLen); + + boolean success = extractAndApplyMetadata(altErs, parentText); + + if (success) { + i = j + 1; + } else { + i++; + } + } + + ers = resolveImplicitRelativeDatePeriod(ers, text); + ers = pruneInvalidImplicitDate(ers); + + return ers; + } + + private List getAltErsWithSameParentText(List ers, int startIndex, String text) { + int pivot = startIndex + 1; + HashSet types = new HashSet(); + types.add(ers.get(startIndex).getType()); + + while (pivot < ers.size()) { + // Currently only support merge two kinds of types + if (!types.contains(ers.get(pivot).getType()) && types.size() > 1) { + break; + } + + // Check whether middle string is a connector + int middleBegin = ers.get(pivot - 1).getStart() + ers.get(pivot - 1).getLength(); + int middleEnd = ers.get(pivot).getStart(); + + if (!isConnectorOrWhiteSpace(middleBegin, middleEnd, text)) { + break; + } + + int prefixEnd = ers.get(pivot - 1).getStart(); + String prefixStr = text.substring(0, prefixEnd); + + if (isEndsWithRangePrefix(prefixStr)) { + break; + } + + if (isSupportedAltEntitySequence(ers.subList(startIndex, startIndex + (pivot - startIndex + 1)))) { + types.add(ers.get(pivot).getType()); + pivot++; + } else { + break; + } + } + + pivot--; + + if (startIndex == pivot) { + startIndex++; + } + + return ers.subList(startIndex, startIndex + (pivot - startIndex + 1)); + } + + private List addImplicitDates(List originalErs, String text) { + List result = new ArrayList<>(); + + Match[] implicitDateMatches = RegExpUtility.getMatches(config.getDayRegex(), text); + int i = 0; + originalErs.sort(Comparator.comparingInt(er -> er.getStart())); + + for (Match dateMatch : implicitDateMatches) { + boolean notBeContained = true; + while (i < originalErs.size()) { + if (originalErs.get(i).getStart() <= dateMatch.index && originalErs.get(i).getStart() + originalErs.get(i).getLength() >= dateMatch.index + dateMatch.length) { + notBeContained = false; + break; + } + + if (originalErs.get(i).getStart() + originalErs.get(i).getLength() < dateMatch.index + dateMatch.length) { + i++; + } else if (originalErs.get(i).getStart() + originalErs.get(i).getLength() >= dateMatch.index + dateMatch.length) { + break; + } + } + + ExtractResult dateEr = new ExtractResult( + dateMatch.index, + dateMatch.length, + dateMatch.value, + Constants.SYS_DATETIME_DATE); + + dateEr.setData(getExtractorName()); + if (notBeContained) { + result.add(dateEr); + } else if (i + 1 < originalErs.size()) { + // For cases like "I am looking at 18 and 19 June" + // in which "18" is wrongly recognized as time without context. + ExtractResult nextEr = originalErs.get(i + 1); + if (nextEr.getType().equals(Constants.SYS_DATETIME_DATE) && + originalErs.get(i).getText().equals(dateEr.getText()) && + isConnectorOrWhiteSpace(dateEr.getStart() + dateEr.getLength(), nextEr.getStart(), text)) { + result.add(dateEr); + originalErs.remove(i); + } + } + } + + result.addAll(originalErs); + result.sort(Comparator.comparingInt(er -> er.getStart())); + + return result; + } + + private List pruneInvalidImplicitDate(List ers) { + ers.removeIf(er -> { + if (er.getData() != null && er.getType().equals(Constants.SYS_DATETIME_DATE) && er.getData().equals(getExtractorName())) { + return true; + } + return false; + }); + + return ers; + } + + // Resolve cases like "this week or next". + private List resolveImplicitRelativeDatePeriod(List ers, String text) { + List relativeTermsMatches = new ArrayList<>(); + for (Pattern regex : config.getRelativePrefixList()) { + relativeTermsMatches.addAll(Arrays.asList(RegExpUtility.getMatches(regex, text))); + } + + List results = new ArrayList<>(); + + List relativeDatePeriodErs = new ArrayList<>(); + int i = 0; + for (ExtractResult result : ers.toArray(new ExtractResult[0])) { + if (!result.getType().equals(Constants.SYS_DATETIME_DATETIMEALT)) { + int resultEnd = result.getStart() + result.getLength(); + for (Match relativeTermsMatch : relativeTermsMatches) { + int relativeTermsMatchEnd = relativeTermsMatch.index + relativeTermsMatch.length; + if (relativeTermsMatch.index > resultEnd || relativeTermsMatchEnd < result.getStart()) { + // Check whether middle string is a connector + int middleBegin = relativeTermsMatch.index > resultEnd ? resultEnd : relativeTermsMatchEnd; + int middleEnd = relativeTermsMatch.index > resultEnd ? relativeTermsMatch.index : result.getStart(); + String middleStr = text.substring(middleBegin, middleEnd).trim().toLowerCase(); + Match[] orTermMatches = RegExpUtility.getMatches(config.getOrRegex(), middleStr); + if (orTermMatches.length == 1 && orTermMatches[0].index == 0 && orTermMatches[0].length == middleStr.length()) { + int parentTextStart = relativeTermsMatch.index > resultEnd ? result.getStart() : relativeTermsMatch.index; + int parentTextEnd = relativeTermsMatch.index > resultEnd ? relativeTermsMatchEnd : resultEnd; + String parentText = text.substring(parentTextStart, parentTextEnd); + + ExtractResult contextErs = new ExtractResult(); + for (Pattern regex : config.getRelativePrefixList()) { + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, result.getText())).findFirst(); + if (match.isPresent()) { + int matchEnd = match.get().index + match.get().length; + contextErs = new ExtractResult( + matchEnd, + result.getLength() - matchEnd, + result.getText().substring(matchEnd, result.getLength()), + Constants.ContextType_RelativeSuffix); + break; + } + } + + Map customData = new LinkedHashMap<>(); + customData.put(Constants.SubType, result.getType()); + customData.put(ExtendedModelResult.ParentTextKey, parentText); + customData.put(Constants.Context, contextErs); + + relativeDatePeriodErs.add(new ExtractResult( + relativeTermsMatch.index, + relativeTermsMatch.length, + relativeTermsMatch.value, + Constants.SYS_DATETIME_DATETIMEALT, + customData)); + + Map resultData = new LinkedHashMap<>(); + resultData.put(Constants.SubType, result.getType()); + resultData.put(ExtendedModelResult.ParentTextKey, parentText); + + result.setData(resultData); + result.setType(Constants.SYS_DATETIME_DATETIMEALT); + ers.set(i, result); + } + } + } + } + i++; + } + + results.addAll(ers); + results.addAll(relativeDatePeriodErs); + results.sort(Comparator.comparingInt(er -> er.getStart())); + + return results; + } + + private boolean isConnectorOrWhiteSpace(int start, int end, String text) { + if (end <= start) { + return false; + } + + String middleStr = text.substring(start, end).trim().toLowerCase(); + + if (StringUtility.isNullOrEmpty(middleStr)) { + return true; + } + + Match[] orTermMatches = RegExpUtility.getMatches(config.getOrRegex(), middleStr); + + return orTermMatches.length == 1 && orTermMatches[0].index == 0 && orTermMatches[0].length == middleStr.length(); + } + + private boolean isEndsWithRangePrefix(String prefixText) { + return RegexExtension.matchEnd(config.getRangePrefixRegex(), prefixText, true).getSuccess(); + } + + private boolean extractAndApplyMetadata(List extractResults, String parentText) { + boolean success = extractAndApplyMetadata(extractResults, parentText, false); + + if (!success) { + success = extractAndApplyMetadata(extractResults, parentText, true); + } + + if (!success && shouldApplyParentText(extractResults)) { + success = applyParentText(extractResults, parentText); + } + + return success; + } + + private boolean extractAndApplyMetadata(List extractResults, String parentText, boolean reverse) { + if (reverse) { + Collections.reverse(extractResults); + } + + boolean success = false; + + // Currently, we support alt entity sequence only when the second alt entity to the last alt entity share the same type + if (isSupportedAltEntitySequence(extractResults)) { + HashMap metadata = extractMetadata(extractResults.stream().findFirst().get(), parentText, extractResults); + HashMap metadataCandidate = null; + + int i = 0; + while (i < extractResults.size()) { + if (metadata == null) { + break; + } + + int j = i + 1; + + while (j < extractResults.size()) { + metadataCandidate = extractMetadata(extractResults.get(j), parentText, extractResults); + + // No context extracted, the context would follow the previous one + // Such as "Wednesday" in "next Tuesday or Wednesday" + if (metadataCandidate == null) { + j++; + } else { + // Current extraction has context, the context would not follow the previous ones + // Such as "Wednesday" in "next Monday or Tuesday or previous Wednesday" + break; + } + } + List ersShareContext = extractResults.subList(i, j); + applyMetadata(ersShareContext, metadata, parentText); + metadata = metadataCandidate; + + i = j; + success = true; + } + } + + return success; + } + + private boolean shouldApplyParentText(List extractResults) { + boolean shouldApply = false; + + if (isSupportedAltEntitySequence(extractResults)) { + String firstEntityType = extractResults.stream().findFirst().get().getType(); + String lastEntityType = extractResults.get(extractResults.size() - 1).getType(); + + if (firstEntityType.equals(Constants.SYS_DATETIME_DATE) && lastEntityType.equals(Constants.SYS_DATETIME_DATE)) { + shouldApply = true; // "11/20 or 11/22" + } else if (firstEntityType.equals(Constants.SYS_DATETIME_TIME) && lastEntityType.equals(Constants.SYS_DATETIME_TIME)) { + shouldApply = true; // "7 oclock or 8 oclock" + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATETIME) && lastEntityType.equals(Constants.SYS_DATETIME_DATETIME)) { + shouldApply = true; // "Monday 1pm or Tuesday 2pm" + } + } + + return shouldApply; + } + + private boolean applyParentText(List extractResults, String parentText) { + boolean success = false; + + if (isSupportedAltEntitySequence(extractResults)) { + for (int i = 0; i < extractResults.size(); i++) { + ExtractResult extractResult = extractResults.get(i); + Map metadata = createMetadata(extractResult.getType(), parentText, null); + Map data = mergeMetadata(extractResult.getData(), metadata); + extractResult.setData(data); + extractResult.setType(getExtractorName()); + extractResults.set(i, extractResult); + } + + success = true; + } + + return success; + } + + private boolean isSupportedAltEntitySequence(List altEntities) { + Stream subSeq = altEntities.stream().skip(1); + List entityTypes = new ArrayList<>(); + + for (ExtractResult er : subSeq.toArray(ExtractResult[]::new)) { + if (!entityTypes.contains(er.getType())) { + entityTypes.add(er.getType()); + } + } + + return entityTypes.size() == 1; + } + + // This method is to extract metadata from the targeted ExtractResult + // For cases like "next week Monday or Tuesday or previous Wednesday", ExtractMethods can be more than one + private HashMap extractMetadata(ExtractResult targetEr, String parentText, List ers) { + HashMap metadata = null; + ArrayList>> extractMethods = getExtractMethods(targetEr.getType(), ers.get(ers.size() - 1).getType()); + BiConsumer postProcessMethod = getPostProcessMethod(targetEr.getType(), ers.get(ers.size() - 1).getType()); + ExtractResult contextEr = extractContext(targetEr, extractMethods, postProcessMethod); + + if (shouldCreateMetadata(ers, contextEr)) { + metadata = createMetadata(targetEr.getType(), parentText, contextEr); + } + + return metadata; + } + + private ExtractResult extractContext(ExtractResult er, ArrayList>> extractMethods, BiConsumer postProcessMethod) { + ExtractResult contextEr = null; + + for (Function> extractMethod : extractMethods) { + List contextErCandidates = extractMethod.apply(er.getText()); + if (contextErCandidates.size() == 1) { + contextEr = contextErCandidates.get(contextErCandidates.size() - 1); + break; + } + } + + if (contextEr != null && postProcessMethod != null) { + postProcessMethod.accept(contextEr, er); + } + + if (contextEr != null && StringUtility.isNullOrEmpty(contextEr.getText())) { + contextEr = null; + } + + return contextEr; + } + + private boolean shouldCreateMetadata(List originalErs, ExtractResult contextEr) { + // For alternative entities sequence which are all DatePeriod, we should create metadata even if context is null + return (contextEr != null || + (originalErs.get(0).getType() == Constants.SYS_DATETIME_DATEPERIOD && originalErs.get(originalErs.size() - 1).getType() == Constants.SYS_DATETIME_DATEPERIOD)); + } + + private void applyMetadata(List ers, HashMap metadata, String parentText) { + // The first extract results don't need any context + ExtractResult first = ers.stream().findFirst().orElse(null); + + // Share the timeZone info + Map metaDataOrigin = (HashMap)first.getData(); + if (metaDataOrigin != null && metaDataOrigin.containsKey(Constants.SYS_DATETIME_TIMEZONE)) { + metadata.put(Constants.SYS_DATETIME_TIMEZONE, metaDataOrigin.get(Constants.SYS_DATETIME_TIMEZONE)); + } + + HashMap metadataWithoutContext = createMetadata(first.getType(), parentText, null); + first.setData(mergeMetadata(first.getData(), metadataWithoutContext)); + first.setType(getExtractorName()); + ers.set(0, first); + + for (int i = 1; i < ers.size(); i++) { + ExtractResult er = ers.get(i); + er.setData(mergeMetadata(ers.get(i).getData(), metadata)); + er.setType(getExtractorName()); + ers.set(i, er); + } + } + + private Map mergeMetadata(Object originalMetadata, Map newMetadata) { + Map result = new HashMap<>(); + + if (originalMetadata instanceof Map) { + result = (Map)originalMetadata; + } + + if (originalMetadata == null) { + result = newMetadata; + } else { + for (Map.Entry data : newMetadata.entrySet()) { + result.put(data.getKey(), data.getValue()); + } + } + + return result; + } + + private HashMap createMetadata(String subType, String parentText, ExtractResult contextEr) { + HashMap data = new HashMap<>(); + + if (!StringUtility.isNullOrEmpty(subType)) { + data.put(Constants.SubType, subType); + } + + if (!StringUtility.isNullOrEmpty(parentText)) { + data.put(ExtendedModelResult.ParentTextKey, parentText); + } + + if (contextEr != null) { + data.put(Constants.Context, contextEr); + } + + return data; + } + + private List extractRelativePrefixContext(String entityText) { + List results = new ArrayList<>(); + + for (Pattern pattern : config.getRelativePrefixList()) { + Matcher match = pattern.matcher(entityText); + + if (match.find()) { + ExtractResult er = new ExtractResult(); + er.setText(match.group()); + er.setStart(match.start()); + er.setLength(match.end() - match.start()); + er.setType(Constants.ContextType_RelativePrefix); + results.add(er); + } + } + + return results; + } + + private List extractAmPmContext(String entityText) { + List results = new ArrayList<>(); + for (Pattern pattern : config.getAmPmRegexList()) { + Matcher match = pattern.matcher(entityText); + if (match.find()) { + ExtractResult er = new ExtractResult(); + er.setText(match.group()); + er.setStart(match.start()); + er.setLength(match.end() - match.start()); + er.setType(Constants.ContextType_AmPm); + results.add(er); + } + } + + return results; + } + + private BiConsumer getPostProcessMethod(String firstEntityType, String lastEntityType) { + if (firstEntityType.equals(Constants.SYS_DATETIME_DATETIMEPERIOD) && lastEntityType.equals(Constants.SYS_DATETIME_DATE)) { + return (contextEr, originalEr) -> { + contextEr.setText(originalEr.getText().substring(0, contextEr.getStart()) + originalEr.getText().substring(contextEr.getStart() + contextEr.getLength())); + contextEr.setType(Constants.ContextType_RelativeSuffix); + }; + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATE) && lastEntityType.equals(Constants.SYS_DATETIME_DATEPERIOD)) { + return (contextEr, originalEr) -> { + contextEr.setText(originalEr.getText().substring(0, contextEr.getStart())); + }; + } + + return null; + } + + private ArrayList>> getExtractMethods(String firstEntityType, String lastEntityType) { + ArrayList>> methods = new ArrayList<>(); + + if (firstEntityType.equals(Constants.SYS_DATETIME_DATETIME) && lastEntityType.equals(Constants.SYS_DATETIME_TIME)) { + + // "Monday 7pm or 8pm" + methods.add(config.getDateExtractor()::extract); + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATE) && lastEntityType.equals(Constants.SYS_DATETIME_DATE)) { + + // "next week Monday or Tuesday", "previous Monday or Wednesday" + methods.add(config.getDatePeriodExtractor()::extract); + methods.add(this::extractRelativePrefixContext); + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_TIME) && lastEntityType.equals(Constants.SYS_DATETIME_TIME)) { + + // "in the morning at 7 oclock or 8 oclock" + methods.add(this::extractAmPmContext); + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATETIME) && lastEntityType.equals(Constants.SYS_DATETIME_DATETIME)) { + + // "next week Mon 9am or Tue 1pm" + methods.add(config.getDatePeriodExtractor()::extract); + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATETIMEPERIOD) && lastEntityType.equals(Constants.SYS_DATETIME_TIMEPERIOD)) { + + // "Monday 7-8 am or 9-10am" + methods.add(config.getDateExtractor()::extract); + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATEPERIOD) && lastEntityType.equals(Constants.SYS_DATETIME_DATEPERIOD)) { + + // For alt entities that are all DatePeriod, no need to share context + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATETIMEPERIOD) && lastEntityType.equals(Constants.SYS_DATETIME_DATE)) { + + // "Tuesday or Wednesday morning" + methods.add(config.getDateExtractor()::extract); + + } else if (firstEntityType.equals(Constants.SYS_DATETIME_DATE) && lastEntityType.equals(Constants.SYS_DATETIME_DATEPERIOD)) { + + // "Monday this week or next week" + methods.add(config.getDatePeriodExtractor()::extract); + + } + + return methods; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimeExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimeExtractor.java new file mode 100644 index 000000000..f54bbbcb4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimeExtractor.java @@ -0,0 +1,308 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.AgoLaterUtil; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +public class BaseDateTimeExtractor implements IDateTimeExtractor { + private static final String SYS_NUM_INTEGER = com.microsoft.recognizers.text.number.Constants.SYS_NUM; + + private final IDateTimeExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DATETIME; + } + + public BaseDateTimeExtractor(IDateTimeExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + tokens.addAll(mergeDateAndTime(input, reference)); + tokens.addAll(basicRegexMatch(input)); + tokens.addAll(timeOfTodayBefore(input, reference)); + tokens.addAll(timeOfTodayAfter(input, reference)); + tokens.addAll(specialTimeOfDate(input, reference)); + tokens.addAll(durationWithBeforeAndAfter(input, reference)); + + return Token.mergeAllTokens(tokens, input, getExtractorName()); + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + private List durationWithBeforeAndAfter(String input, LocalDateTime reference) { + List ret = new ArrayList<>(); + + List ers = this.config.getDurationExtractor().extract(input, reference); + for (ExtractResult er : ers) { + // if it is a multiple duration and its type is equal to Date then skip it. + if (er.getData() != null && er.getData().toString() == Constants.MultipleDuration_Date) { + continue; + } + + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getUnitRegex(), er.getText())).findFirst(); + if (!match.isPresent()) { + continue; + } + + ret = AgoLaterUtil.extractorDurationWithBeforeAndAfter(input, er, ret, this.config.getUtilityConfiguration()); + } + + return ret; + } + + public List specialTimeOfDate(String input, LocalDateTime reference) { + List ret = new ArrayList<>(); + List ers = this.config.getDatePointExtractor().extract(input, reference); + + // handle "the end of the day" + for (ExtractResult er : ers) { + String beforeStr = input.substring(0, (er != null) ? er.getStart() : 0); + + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getSpecificEndOfRegex(), beforeStr)).findFirst(); + if (match.isPresent()) { + ret.add(new Token(match.get().index, (er != null) ? er.getStart() + er.getLength() : 0)); + } else { + String afterStr = input.substring((er != null) ? er.getStart() + er.getLength() : 0); + + match = Arrays.stream(RegExpUtility.getMatches(this.config.getSpecificEndOfRegex(), afterStr)).findFirst(); + if (match.isPresent()) { + ret.add(new Token( + (er != null) ? er.getStart() : 0, + ((er != null) ? er.getStart() + er.getLength() : 0) + ((match != null) ? match.get().index + match.get().length : 0))); + } + } + } + + // Handle "eod, end of day" + Match[] matches = RegExpUtility.getMatches(config.getUnspecificEndOfRegex(), input); + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + + return ret; + } + + // Parses a specific time of today, tonight, this afternoon, like "seven this afternoon" + public List timeOfTodayAfter(String input, LocalDateTime reference) { + List ret = new ArrayList<>(); + + List ers = this.config.getTimePointExtractor().extract(input, reference); + + for (ExtractResult er : ers) { + String afterStr = input.substring(er.getStart() + er.getLength()); + + if (StringUtility.isNullOrEmpty(afterStr)) { + continue; //@here + } + + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getTimeOfTodayAfterRegex(), afterStr)).findFirst(); + if (match.isPresent()) { + int begin = er.getStart(); + int end = er.getStart() + er.getLength() + match.get().length; + ret.add(new Token(begin, end)); + } + } + + Match[] matches = RegExpUtility.getMatches(this.config.getSimpleTimeOfTodayAfterRegex(), input); + for (Match match : matches) { + // @TODO Remove when lookbehinds are handled correctly + if (isDecimal(match, input)) { + continue; + } + + ret.add(new Token(match.index, match.index + match.length)); + } + + return ret; + } + + // Check if the match is part of a decimal number (e.g. 123.24) + private boolean isDecimal(Match match, String text) { + boolean isDecimal = false; + if (match.index > 1 && (text.charAt(match.index - 1) == ',' || + text.charAt(match.index - 1) == '.') && Character.isDigit(text.charAt(match.index - 2)) && Character.isDigit(match.value.charAt(0))) { + isDecimal = true; + } + + return isDecimal; + } + + public List timeOfTodayBefore(String input, LocalDateTime reference) { + List ret = new ArrayList<>(); + List ers = this.config.getTimePointExtractor().extract(input, reference); + for (ExtractResult er : ers) { + String beforeStr = input.substring(0, (er != null) ? er.getStart() : 0); + + // handle "this morningh at 7am" + ConditionalMatch innerMatch = RegexExtension.matchBegin(this.config.getTimeOfDayRegex(), er.getText(), true); + if (innerMatch.getSuccess()) { + beforeStr = input.substring(0, ((er != null) ? er.getStart() : 0) + innerMatch.getMatch().get().length); + } + + if (StringUtility.isNullOrEmpty(beforeStr)) { + continue; + } + + Match match = Arrays.stream(RegExpUtility.getMatches(this.config.getTimeOfTodayBeforeRegex(), beforeStr)).findFirst().orElse(null); + if (match != null) { + int begin = match.index; + int end = er.getStart() + er.getLength(); + ret.add(new Token(begin, end)); + } + } + + Match[] matches = RegExpUtility.getMatches(this.config.getSimpleTimeOfTodayBeforeRegex(), input); + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + + return ret; + } + + // Match "now" + public List basicRegexMatch(String input) { + List ret = new ArrayList<>(); + input = input.trim().toLowerCase(); + + // Handle "now" + Match[] matches = RegExpUtility.getMatches(this.config.getNowRegex(), input); + + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + + return ret; + } + + // Merge a Date entity and a Time entity, like "at 7 tomorrow" + public List mergeDateAndTime(String input, LocalDateTime reference) { + List ret = new ArrayList<>(); + List dateErs = this.config.getDatePointExtractor().extract(input, reference); + if (dateErs.size() == 0) { + return ret; + } + + List timeErs = this.config.getTimePointExtractor().extract(input, reference); + Match[] timeNumMatches = RegExpUtility.getMatches(config.getNumberAsTimeRegex(), input); + + if (timeErs.size() == 0 && timeNumMatches.length == 0) { + return ret; + } + + List ers = dateErs; + ers.addAll(timeErs); + + // handle cases which use numbers as time points + // only enabled in CalendarMode + if (this.config.getOptions().match(DateTimeOptions.CalendarMode)) { + List numErs = new ArrayList<>(); + for (Match timeNumMatch : timeNumMatches) { + ExtractResult node = new ExtractResult(timeNumMatch.index, timeNumMatch.length, timeNumMatch.value, SYS_NUM_INTEGER); + numErs.add(node); + } + ers.addAll(numErs); + } + + ers.sort(Comparator.comparingInt(erA -> erA.getStart())); + + int i = 0; + while (i < ers.size() - 1) { + int j = i + 1; + while (j < ers.size() && ers.get(i).isOverlap(ers.get(j))) { + j++; + } + if (j >= ers.size()) { + break; + } + + ExtractResult ersI = ers.get(i); + ExtractResult ersJ = ers.get(j); + if (ersI.getType() == Constants.SYS_DATETIME_DATE && ersJ.getType() == Constants.SYS_DATETIME_TIME || + ersI.getType() == Constants.SYS_DATETIME_TIME && ersJ.getType() == Constants.SYS_DATETIME_DATE || + ersI.getType() == Constants.SYS_DATETIME_DATE && ersJ.getType() == SYS_NUM_INTEGER) { + int middleBegin = ersI != null ? ersI.getStart() + ersI.getLength() : 0; + int middleEnd = ersJ != null ? ersJ.getStart() : 0; + if (middleBegin > middleEnd) { + i = j + 1; + continue; + } + + String middleStr = input.substring(middleBegin, middleEnd).trim().toLowerCase(); + boolean valid = false; + // for cases like "tomorrow 3", "tomorrow at 3" + if (ersJ.getType() == SYS_NUM_INTEGER) { + Optional matches = Arrays.stream(RegExpUtility.getMatches(this.config.getDateNumberConnectorRegex(), input)).findFirst(); + if (StringUtility.isNullOrEmpty(middleStr) || matches.isPresent()) { + valid = true; + } + } else { + // For case like "3pm or later on monday" + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getSuffixAfterRegex(), middleStr)).findFirst(); + if (match.isPresent()) { + middleStr = middleStr.substring(match.get().index + match.get().length).trim(); + } + + if (!(match.isPresent() && middleStr.isEmpty())) { + if (this.config.isConnector(middleStr)) { + valid = true; + } + } + } + + if (valid) { + int begin = ersI.getStart(); + int end = ersJ.getStart() + ersJ.getLength(); + ret.add(new Token(begin, end)); + i = j + 1; + continue; + } + } + i = j; + } + + // Handle "in the afternoon" at the end of entity + for (int idx = 0; idx < ret.size(); idx++) { + Token idxToken = ret.get(idx); + String afterStr = input.substring(idxToken.getEnd()); + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getSuffixRegex(), afterStr)).findFirst(); + if (match.isPresent()) { + ret.set(idx, new Token(idxToken.getStart(), idxToken.getEnd() + match.get().length)); + } + } + + // Handle "day" prefixes + for (int idx = 0; idx < ret.size(); idx++) { + Token idxToken = ret.get(idx); + String beforeStr = input.substring(0, idxToken.getStart()); + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getUtilityConfiguration().getCommonDatePrefixRegex(), beforeStr)).findFirst(); + if (match.isPresent()) { + ret.set(idx, new Token(idxToken.getStart() - match.get().length, idxToken.getEnd())); + } + } + + return ret; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimePeriodExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimePeriodExtractor.java new file mode 100644 index 000000000..d4b8b7b5c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDateTimePeriodExtractor.java @@ -0,0 +1,575 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneUtility; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class BaseDateTimePeriodExtractor implements IDateTimeExtractor { + + private final IDateTimePeriodExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DATETIMEPERIOD; + } + + public BaseDateTimePeriodExtractor(IDateTimePeriodExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + @Override + public List extract(String input, LocalDateTime reference) { + + List tokens = new ArrayList<>(); + + // Date and time Extractions should be extracted from the text only once, and shared in the methods below, passed by value + List dateErs = config.getSingleDateExtractor().extract(input, reference); + List timeErs = config.getSingleTimeExtractor().extract(input, reference); + + tokens.addAll(matchSimpleCases(input, reference)); + tokens.addAll(mergeTwoTimePoints(input, reference, new ArrayList(dateErs), new ArrayList(timeErs))); + tokens.addAll(matchDuration(input, reference)); + tokens.addAll(matchTimeOfDay(input, reference, new ArrayList(dateErs))); + tokens.addAll(matchRelativeUnit(input)); + tokens.addAll(matchDateWithPeriodPrefix(input, reference, new ArrayList(dateErs))); + tokens.addAll(mergeDateWithTimePeriodSuffix(input, new ArrayList(dateErs), new ArrayList(timeErs))); + + List ers = Token.mergeAllTokens(tokens, input, getExtractorName()); + + if (config.getOptions().match(DateTimeOptions.EnablePreview)) { + ers = TimeZoneUtility.mergeTimeZones(ers, config.getTimeZoneExtractor().extract(input, reference), input); + } + + return ers; + } + + private List matchSimpleCases(String input, LocalDateTime reference) { + List results = new ArrayList<>(); + + for (Pattern regex : config.getSimpleCasesRegex()) { + Match[] matches = RegExpUtility.getMatches(regex, input); + for (Match match : matches) { + // Is there a date before it? + boolean hasBeforeDate = false; + String beforeStr = input.substring(0, match.index); + if (!StringUtility.isNullOrEmpty(beforeStr)) { + List ers = config.getSingleDateExtractor().extract(beforeStr, reference); + if (ers.size() > 0) { + ExtractResult er = ers.get(ers.size() - 1); + int begin = er.getStart(); + String middleStr = beforeStr.substring(begin + er.getLength()).trim().toLowerCase(); + if (StringUtility.isNullOrEmpty(middleStr) || RegexExtension.isExactMatch(config.getPrepositionRegex(), middleStr, true)) { + results.add(new Token(begin, match.index + match.length)); + hasBeforeDate = true; + } + } + } + + String followedStr = input.substring(match.index + match.length); + if (!StringUtility.isNullOrEmpty(followedStr) && !hasBeforeDate) { + // Is it followed by a date? + List ers = config.getSingleDateExtractor().extract(followedStr, reference); + if (ers.size() > 0) { + ExtractResult er = ers.get(0); + int begin = er.getStart(); + int end = er.getStart() + er.getLength(); + String middleStr = followedStr.substring(0, begin).trim().toLowerCase(); + if (StringUtility.isNullOrEmpty(middleStr) || RegexExtension.isExactMatch(config.getPrepositionRegex(), middleStr, true)) { + results.add(new Token(match.index, match.index + match.length + end)); + } + } + } + } + } + + return results; + } + + private List mergeTwoTimePoints(String input, LocalDateTime reference, List dateErs, List timeErs) { + + List results = new ArrayList<>(); + List dateTimeErs = config.getSingleDateTimeExtractor().extract(input, reference); + List timePoints = new ArrayList<>(); + + // Handle the overlap problem + int j = 0; + for (ExtractResult er : dateTimeErs) { + timePoints.add(er); + + while (j < timeErs.size() && timeErs.get(j).getStart() + timeErs.get(j).getLength() < er.getStart()) { + timePoints.add(timeErs.get(j)); + j++; + } + + while (j < timeErs.size() && timeErs.get(j).isOverlap(er)) { + j++; + } + } + + for (; j < timeErs.size(); j++) { + timePoints.add(timeErs.get(j)); + } + + timePoints.sort(Comparator.comparingInt(arg -> arg.getStart())); + + // Merge "{TimePoint} to {TimePoint}", "between {TimePoint} and {TimePoint}" + int idx = 0; + while (idx < timePoints.size() - 1) { + // If both ends are Time. then this is a TimePeriod, not a DateTimePeriod + if (timePoints.get(idx).getType().equals(Constants.SYS_DATETIME_TIME) && timePoints.get(idx + 1).getType().equals(Constants.SYS_DATETIME_TIME)) { + idx++; + continue; + } + + int middleBegin = timePoints.get(idx).getStart() + timePoints.get(idx).getLength(); + int middleEnd = timePoints.get(idx + 1).getStart(); + + String middleStr = input.substring(middleBegin, middleEnd).trim(); + + // Handle "{TimePoint} to {TimePoint}" + if (RegexExtension.isExactMatch(config.getTillRegex(), middleStr, true)) { + int periodBegin = timePoints.get(idx).getStart(); + int periodEnd = timePoints.get(idx + 1).getStart() + timePoints.get(idx + 1).getLength(); + + // Handle "from" + String beforeStr = input.substring(0, periodBegin).trim().toLowerCase(); + + ResultIndex fromIndex = config.getFromTokenIndex(beforeStr); + ResultIndex betweenIndex = config.getBetweenTokenIndex(beforeStr); + if (fromIndex.getResult()) { + periodBegin = fromIndex.getIndex(); + } else if (betweenIndex.getResult()) { + periodBegin = betweenIndex.getIndex(); + } + + results.add(new Token(periodBegin, periodEnd)); + idx += 2; + continue; + } + + // Handle "between {TimePoint} and {TimePoint}" + if (config.hasConnectorToken(middleStr)) { + int periodBegin = timePoints.get(idx).getStart(); + int periodEnd = timePoints.get(idx + 1).getStart() + timePoints.get(idx + 1).getLength(); + + // Handle "between" + String beforeStr = input.substring(0, periodBegin).trim().toLowerCase(); + + ResultIndex betweenIndex = config.getBetweenTokenIndex(beforeStr); + if (betweenIndex.getResult()) { + periodBegin = betweenIndex.getIndex(); + results.add(new Token(periodBegin, periodEnd)); + idx += 2; + continue; + } + } + idx++; + } + + // Regarding the pharse as-- {Date} {TimePeriod}, like "2015-9-23 1pm to 4" + // Or {TimePeriod} on {Date}, like "1:30 to 4 on 2015-9-23" + List timePeriodErs = config.getTimePeriodExtractor().extract(input, reference); + dateErs.addAll(timePeriodErs); + + dateErs.sort(Comparator.comparingInt(arg -> arg.getStart())); + + for (idx = 0; idx < dateErs.size() - 1; idx++) { + if (dateErs.get(idx).getType().equals(dateErs.get(idx + 1).getType())) { + continue; + } + + int midBegin = dateErs.get(idx).getStart() + dateErs.get(idx).getLength(); + int midEnd = dateErs.get(idx + 1).getStart(); + + if (midEnd - midBegin > 0) { + String midStr = input.substring(midBegin, midEnd); + if (StringUtility.isNullOrWhiteSpace(midStr) || StringUtility.trimStart(midStr).startsWith(config.getTokenBeforeDate())) { + // Extend date extraction for cases like "Monday evening next week" + String extendedStr = dateErs.get(idx).getText() + input.substring(dateErs.get(idx + 1).getStart() + dateErs.get(idx + 1).getLength()); + Optional extendedDateEr = config.getSingleDateExtractor().extract(extendedStr).stream().findFirst(); + int offset = 0; + + if (extendedDateEr.isPresent() && extendedDateEr.get().getStart() == 0) { + offset = extendedDateEr.get().getLength() - dateErs.get(idx).getLength(); + } + + results.add(new Token(dateErs.get(idx).getStart(), offset + dateErs.get(idx + 1).getStart() + dateErs.get(idx + 1).getLength())); + idx += 2; + } + } + } + + return results; + } + + //TODO: this can be abstracted with the similar method in BaseDatePeriodExtractor + private List matchDuration(String input, LocalDateTime reference) { + List results = new ArrayList<>(); + + List durations = new ArrayList<>(); + List durationErs = config.getDurationExtractor().extract(input, reference); + + for (ExtractResult durationEr : durationErs) { + Optional match = match(config.getTimeUnitRegex(), durationEr.getText()); + if (match.isPresent()) { + durations.add(new Token(durationEr.getStart(), durationEr.getStart() + durationEr.getLength())); + } + } + + for (Token duration : durations) { + String beforeStr = input.substring(0, duration.getStart()).toLowerCase(); + String afterStr = input.substring(duration.getStart() + duration.getLength()); + + if (StringUtility.isNullOrWhiteSpace(beforeStr) && StringUtility.isNullOrWhiteSpace(afterStr)) { + continue; + } + + // within (the) (next) "Seconds/Minutes/Hours" should be handled as datetimeRange here + // within (the) (next) XX days/months/years + "Seconds/Minutes/Hours" should also be handled as datetimeRange here + Optional match = match(config.getWithinNextPrefixRegex(), beforeStr); + if (matchPrefixRegexInSegment(beforeStr, match)) { + int startToken = match.get().index; + int durationLength = duration.getStart() + duration.getLength(); + match = match(config.getTimeUnitRegex(), input.substring(duration.getStart(), durationLength)); + if (match.isPresent()) { + results.add(new Token(startToken, duration.getEnd())); + continue; + } + } + + int index = -1; + + match = match(config.getPastPrefixRegex(), beforeStr); + if (match.isPresent() && StringUtility.isNullOrWhiteSpace(beforeStr.substring(match.get().index + match.get().length))) { + index = match.get().index; + } + + if (index < 0) { + match = match(config.getNextPrefixRegex(), beforeStr); + if (match.isPresent() && StringUtility.isNullOrWhiteSpace(beforeStr.substring(match.get().index + match.get().length))) { + index = match.get().index; + } + } + + if (index >= 0) { + + String prefix = beforeStr.substring(0, index).trim(); + String durationText = input.substring(duration.getStart(), duration.getStart() + duration.getLength()); + List numbersInPrefix = config.getCardinalExtractor().extract(prefix); + List numbersInDuration = config.getCardinalExtractor().extract(durationText); + + // Cases like "2 upcoming days", should be supported here + // Cases like "2 upcoming 3 days" is invalid, only extract "upcoming 3 days" by default + if (!numbersInPrefix.isEmpty() && numbersInDuration.isEmpty()) { + ExtractResult lastNumber = numbersInPrefix.stream() + .sorted(Comparator.comparingInt(x -> x.getStart() + x.getLength())) + .reduce((acc, item) -> item).orElse(null); + + // Prefix should ends with the last number + if (lastNumber.getStart() + lastNumber.getLength() == prefix.length()) { + results.add(new Token(lastNumber.getStart(), duration.getEnd())); + } + + } else { + results.add(new Token(index, duration.getEnd())); + } + + continue; + } + + Optional matchDateUnit = Arrays.stream(RegExpUtility.getMatches(config.getDateUnitRegex(), afterStr)).findFirst(); + if (!matchDateUnit.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getPastPrefixRegex(), afterStr)).findFirst(); + if (match.isPresent() && StringUtility.isNullOrWhiteSpace(afterStr.substring(0, match.get().index))) { + results.add(new Token(duration.getStart(), duration.getStart() + duration.getLength() + match.get().index + match.get().length)); + continue; + } + + match = Arrays.stream(RegExpUtility.getMatches(config.getNextPrefixRegex(), afterStr)).findFirst(); + if (match.isPresent() && StringUtility.isNullOrWhiteSpace(afterStr.substring(0, match.get().index))) { + results.add(new Token(duration.getStart(), duration.getStart() + duration.getLength() + match.get().index + match.get().length)); + continue; + } + + match = Arrays.stream(RegExpUtility.getMatches(config.getFutureSuffixRegex(), afterStr)).findFirst(); + if (match.isPresent() && StringUtility.isNullOrWhiteSpace(afterStr.substring(0, match.get().index))) { + results.add(new Token(duration.getStart(), duration.getStart() + duration.getLength() + match.get().index + match.get().length)); + } + } + } + + return results; + } + + private Optional match(Pattern regex, String input) { + return Arrays.stream(RegExpUtility.getMatches(regex, input)).findFirst(); + } + + private boolean matchPrefixRegexInSegment(String beforeStr, Optional match) { + return match.isPresent() && StringUtility.isNullOrWhiteSpace(beforeStr.substring(match.get().index + match.get().length)); + } + + private List matchTimeOfDay(String input, LocalDateTime reference, List dateErs) { + + List results = new ArrayList<>(); + Match[] matches = RegExpUtility.getMatches(config.getSpecificTimeOfDayRegex(), input); + for (Match match : matches) { + results.add(new Token(match.index, match.index + match.length)); + } + + // Date followed by morning, afternoon or morning, afternoon followed by Date + if (dateErs.size() == 0) { + return results; + } + + for (ExtractResult er : dateErs) { + String afterStr = input.substring(er.getStart() + er.getLength()); + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getPeriodTimeOfDayWithDateRegex(), afterStr)).findFirst(); + + if (match.isPresent()) { + // For cases like "Friday afternoon between 1PM and 4 PM" which "Friday afternoon" need to be extracted first + if (StringUtility.isNullOrWhiteSpace(afterStr.substring(0, match.get().index))) { + int start = er.getStart(); + int end = er.getStart() + er.getLength() + + match.get().getGroup(Constants.TimeOfDayGroupName).index + + match.get().getGroup(Constants.TimeOfDayGroupName).length; + + results.add(new Token(start, end)); + continue; + } + + String connectorStr = afterStr.substring(0, match.get().index); + + // Trim here is set to false as the Regex might catch white spaces before or after the text + boolean isMatch = RegexExtension.isExactMatch(config.getMiddlePauseRegex(), connectorStr, false); + if (isMatch) { + String suffix = StringUtility.trimStart(afterStr.substring(match.get().index + match.get().length)); + + Optional endingMatch = Arrays.stream(RegExpUtility.getMatches(config.getGeneralEndingRegex(), suffix)).findFirst(); + + if (endingMatch.isPresent()) { + results.add(new Token(er.getStart(), er.getStart() + er.getLength() + match.get().index + match.get().length)); + } + } + } + + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getAmDescRegex(), afterStr)).findFirst(); + } + + if (!match.isPresent() || !StringUtility.isNullOrWhiteSpace(afterStr.substring(0, match.get().index))) { + match = Arrays.stream(RegExpUtility.getMatches(config.getPmDescRegex(), afterStr)).findFirst(); + } + + if (match.isPresent()) { + if (StringUtility.isNullOrWhiteSpace(afterStr.substring(0, match.get().index))) { + results.add(new Token(er.getStart(), er.getStart() + er.getLength() + match.get().index + match.get().length)); + } else { + String connectorStr = afterStr.substring(0, match.get().index); + // Trim here is set to false as the Regex might catch white spaces before or after the text + if (RegexExtension.isExactMatch(config.getMiddlePauseRegex(), connectorStr, false)) { + String suffix = afterStr.substring(match.get().index + match.get().length).replaceAll("^\\s+", ""); + + Optional endingMatch = Arrays.stream(RegExpUtility.getMatches(config.getGeneralEndingRegex(), suffix)).findFirst(); + if (endingMatch.isPresent()) { + results.add(new Token(er.getStart(), er.getStart() + er.getLength() + match.get().index + match.get().length)); + } + } + } + } + + String prefixStr = input.substring(0, er.getStart()); + + match = Arrays.stream(RegExpUtility.getMatches(config.getPeriodTimeOfDayWithDateRegex(), prefixStr)).findFirst(); + if (match.isPresent()) { + if (StringUtility.isNullOrWhiteSpace(prefixStr.substring(match.get().index + match.get().length))) { + String midStr = input.substring(match.get().index + match.get().length, er.getStart()); + if (!StringUtility.isNullOrEmpty(midStr) && StringUtility.isNullOrWhiteSpace(midStr)) { + results.add(new Token(match.get().index, er.getStart() + er.getLength())); + } + } else { + String connectorStr = prefixStr.substring(match.get().index + match.get().length); + + // Trim here is set to false as the Regex might catch white spaces before or after the text + if (RegexExtension.isExactMatch(config.getMiddlePauseRegex(), connectorStr, false)) { + String suffix = StringUtility.trimStart(input.substring(er.getStart() + er.getLength())); + + Optional endingMatch = Arrays.stream(RegExpUtility.getMatches(config.getGeneralEndingRegex(), suffix)).findFirst(); + if (endingMatch.isPresent()) { + results.add(new Token(match.get().index, er.getStart() + er.getLength())); + } + } + } + } + } + + // Check whether there are adjacent time period strings, before or after + for (Token result : results.toArray(new Token[0])) { + // Try to extract a time period in before-string + if (result.getStart() > 0) { + String beforeStr = input.substring(0, result.getStart()); + if (!StringUtility.isNullOrEmpty(beforeStr)) { + List timeErs = config.getTimePeriodExtractor().extract(beforeStr); + if (timeErs.size() > 0) { + for (ExtractResult timeEr : timeErs) { + String midStr = beforeStr.substring(timeEr.getStart() + timeEr.getLength()); + if (StringUtility.isNullOrWhiteSpace(midStr)) { + results.add(new Token(timeEr.getStart(), timeEr.getStart() + timeEr.getLength() + midStr.length() + result.getLength())); + } + } + } + } + } + + // Try to extract a time period in after-string + if (result.getStart() + result.getLength() <= input.length()) { + String afterStr = input.substring(result.getStart() + result.getLength()); + if (!StringUtility.isNullOrEmpty(afterStr)) { + List timeErs = config.getTimePeriodExtractor().extract(afterStr); + for (ExtractResult timeEr: timeErs) { + String midStr = afterStr.substring(0, timeEr.getStart()); + if (StringUtility.isNullOrWhiteSpace(midStr)) { + results.add(new Token(result.getStart(), result.getStart() + result.getLength() + midStr.length() + timeEr.getLength())); + } + } + + } + } + } + + return results; + } + + private List matchRelativeUnit(String input) { + List results = new ArrayList(); + + Match[] matches = RegExpUtility.getMatches(config.getRelativeTimeUnitRegex(), input); + if (matches.length == 0) { + matches = RegExpUtility.getMatches(config.getRestOfDateTimeRegex(), input); + } + + for (Match match : matches) { + results.add(new Token(match.index, match.index + match.length)); + } + + return results; + } + + private List matchDateWithPeriodPrefix(String input, LocalDateTime reference, List dateErs) { + List results = new ArrayList(); + + for (ExtractResult dateEr : dateErs) { + int dateStrEnd = dateEr.getStart() + dateEr.getLength(); + String beforeStr = input.substring(0, dateEr.getStart()).trim(); + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getPrefixDayRegex(), beforeStr)).findFirst(); + if (match.isPresent()) { + results.add(new Token(match.get().index, dateStrEnd)); + } + } + + return results; + } + + // Cases like "today after 2:00pm", "1/1/2015 before 2:00 in the afternoon" + private List mergeDateWithTimePeriodSuffix(String input, List dateErs, List timeErs) { + List results = new ArrayList(); + + if (dateErs.isEmpty()) { + return results; + } + + if (timeErs.isEmpty()) { + return results; + } + + List ers = new ArrayList<>(); + ers.addAll(dateErs); + ers.addAll(timeErs); + + ers.sort(Comparator.comparingInt(arg -> arg.getStart())); + + int i = 0; + while (i < ers.size() - 1) { + int j = i + 1; + while (j < ers.size() && ers.get(i).isOverlap(ers.get(j))) { + j++; + } + + if (j >= ers.size()) { + break; + } + + if (ers.get(i).getType().equals(Constants.SYS_DATETIME_DATE) && ers.get(j).getType().equals(Constants.SYS_DATETIME_TIME)) { + int middleBegin = ers.get(i).getStart() + ers.get(i).getLength(); + int middleEnd = ers.get(j).getStart(); + if (middleBegin > middleEnd) { + i = j + 1; + continue; + } + + String middleStr = input.substring(middleBegin, middleEnd).trim().toLowerCase(); + if (isValidConnectorForDateAndTimePeriod(middleStr)) { + int begin = ers.get(i).getStart(); + int end = ers.get(j).getStart() + ers.get(j).getLength(); + results.add(new Token(begin, end)); + } + + i = j + 1; + continue; + } + i = j; + } + + // Handle "in the afternoon" at the end of entity + for (int idx = 0; idx < results.size(); idx++) { + String afterStr = input.substring(results.get(idx).getEnd()); + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getSuffixRegex(), afterStr)).findFirst(); + if (match.isPresent()) { + Token oldToken = results.get(idx); + results.set(idx, new Token(oldToken.getStart(), oldToken.getEnd() + match.get().length)); + } + } + + return results; + } + + // Cases like "today after 2:00pm", "1/1/2015 before 2:00 in the afternoon" + // Valid connector in English for Before include: "before", "no later than", "in advance of", "prior to", "earlier than", "sooner than", "by", "till", "until"... + // Valid connector in English for After include: "after", "later than" + private boolean isValidConnectorForDateAndTimePeriod(String text) { + + List beforeAfterRegexes = new ArrayList<>(); + beforeAfterRegexes.add(config.getBeforeRegex()); + beforeAfterRegexes.add(config.getAfterRegex()); + + for (Pattern regex : beforeAfterRegexes) { + if (RegexExtension.isExactMatch(regex, text, true)) { + return true; + } + } + + return false; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDurationExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDurationExtractor.java new file mode 100644 index 000000000..949aa4c1e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseDurationExtractor.java @@ -0,0 +1,281 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.config.IDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DurationParsingUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BaseDurationExtractor implements IDateTimeExtractor { + + private final IDurationExtractorConfiguration config; + private final boolean merge; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DURATION; + } + + public BaseDurationExtractor(IDurationExtractorConfiguration config) { + this(config, true); + } + + public BaseDurationExtractor(IDurationExtractorConfiguration config, boolean merge) { + this.config = config; + this.merge = merge; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + @Override + public List extract(String input, LocalDateTime reference) { + List tokens = new ArrayList<>(); + + List numberWithUnitTokens = numberWithUnit(input); + + tokens.addAll(numberWithUnitTokens); + tokens.addAll(numberWithUnitAndSuffix(input, numberWithUnitTokens)); + tokens.addAll(implicitDuration(input)); + + List result = Token.mergeAllTokens(tokens, input, getExtractorName()); + + // First MergeMultipleDuration then ResolveMoreThanOrLessThanPrefix so cases like "more than 4 days and less than 1 week" will not be merged into one "multipleDuration" + if (merge) { + result = mergeMultipleDuration(input, result); + } + + result = tagInequalityPrefix(input, result); + + return result; + } + + private List tagInequalityPrefix(String input, List result) { + Stream resultStream = result.stream().map(er -> { + String beforeString = input.substring(0, er.getStart()); + boolean isInequalityPrefixMatched = false; + + ConditionalMatch match = RegexExtension.matchEnd(this.config.getMoreThanRegex(), beforeString, true); + + // The second condition is necessary so for "1 week" in "more than 4 days and less than 1 week", it will not be tagged incorrectly as "more than" + if (match.getSuccess()) { + er.setData(Constants.MORE_THAN_MOD); + isInequalityPrefixMatched = true; + } + + if (!isInequalityPrefixMatched) { + match = RegexExtension.matchEnd(this.config.getLessThanRegex(), beforeString, true); + + if (match.getSuccess()) { + er.setData(Constants.LESS_THAN_MOD); + isInequalityPrefixMatched = true; + } + } + + if (isInequalityPrefixMatched) { + int length = er.getLength() + er.getStart() - match.getMatch().get().index; + int start = match.getMatch().get().index; + String text = input.substring(start, start + length); + er.setStart(start); + er.setLength(length); + er.setText(text); + } + + return er; + }); + return resultStream.collect(Collectors.toList()); + } + + private List mergeMultipleDuration(String input, List extractResults) { + if (extractResults.size() <= 1) { + return extractResults; + } + + ImmutableMap unitMap = config.getUnitMap(); + ImmutableMap unitValueMap = config.getUnitValueMap(); + Pattern unitRegex = config.getDurationUnitRegex(); + + List result = new ArrayList<>(); + + int firstExtractionIndex = 0; + int timeUnit = 0; + int totalUnit = 0; + + while (firstExtractionIndex < extractResults.size()) { + String currentUnit = null; + Optional unitMatch = Arrays.stream(RegExpUtility.getMatches(unitRegex, extractResults.get(firstExtractionIndex).getText())).findFirst(); + + if (unitMatch.isPresent() && unitMap.containsKey(unitMatch.get().getGroup("unit").value)) { + currentUnit = unitMatch.get().getGroup("unit").value; + totalUnit++; + if (DurationParsingUtil.isTimeDurationUnit(unitMap.get(currentUnit))) { + timeUnit++; + } + } + + if (StringUtility.isNullOrEmpty(currentUnit)) { + firstExtractionIndex++; + continue; + } + + int secondExtractionIndex = firstExtractionIndex + 1; + while (secondExtractionIndex < extractResults.size()) { + boolean valid = false; + int midStrBegin = extractResults.get(secondExtractionIndex - 1).getStart() + extractResults.get(secondExtractionIndex - 1).getLength(); + int midStrEnd = extractResults.get(secondExtractionIndex).getStart(); + String midStr = input.substring(midStrBegin, midStrEnd); + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getDurationConnectorRegex(), midStr)).findFirst(); + + if (match.isPresent()) { + unitMatch = Arrays.stream(RegExpUtility.getMatches(unitRegex, extractResults.get(secondExtractionIndex).getText())).findFirst(); + + if (unitMatch.isPresent() && unitMap.containsKey(unitMatch.get().getGroup("unit").value)) { + String nextUnitStr = unitMatch.get().getGroup("unit").value; + + if (unitValueMap.get(nextUnitStr) != unitValueMap.get(currentUnit)) { + valid = true; + + if (unitValueMap.get(nextUnitStr) < unitValueMap.get(currentUnit)) { + currentUnit = nextUnitStr; + } + } + + totalUnit++; + + if (DurationParsingUtil.isTimeDurationUnit(unitMap.get(nextUnitStr))) { + timeUnit++; + } + } + } + + if (!valid) { + break; + } + + secondExtractionIndex++; + } + + if (secondExtractionIndex - 1 > firstExtractionIndex) { + int start = extractResults.get(firstExtractionIndex).getStart(); + int length = extractResults.get(secondExtractionIndex - 1).getStart() + extractResults.get(secondExtractionIndex - 1).getLength() - start; + String text = input.substring(start, start + length); + String rType = extractResults.get(firstExtractionIndex).getType(); + ExtractResult node = new ExtractResult(start, length, text, rType, null); + + // add multiple duration type to extract result + String type = null; + + if (timeUnit == totalUnit) { + type = Constants.MultipleDuration_Time; + } else if (timeUnit == 0) { + type = Constants.MultipleDuration_Date; + } else { + type = Constants.MultipleDuration_DateTime; + } + + node.setData(type); + result.add(node); + + timeUnit = 0; + totalUnit = 0; + + } else { + result.add(extractResults.get(firstExtractionIndex)); + } + + firstExtractionIndex = secondExtractionIndex; + } + + return result; + } + + // handle cases that don't contain nubmer + private Collection implicitDuration(String text) { + Collection result = new ArrayList<>(); + + // handle "all day", "all year" + result.addAll(getTokenFromRegex(config.getAllRegex(), text)); + + // handle "half day", "half year" + result.addAll(getTokenFromRegex(config.getHalfRegex(), text)); + + // handle "next day", "last year" + result.addAll(getTokenFromRegex(config.getRelativeDurationUnitRegex(), text)); + + // handle "during/for the day/week/month/year" + if (config.getOptions().match(DateTimeOptions.CalendarMode)) { + result.addAll(getTokenFromRegex(config.getDuringRegex(), text)); + } + + return result; + } + + // simple cases made by a number followed an unit + private List numberWithUnit(String text) { + List result = new ArrayList<>(); + List ers = this.config.getCardinalExtractor().extract(text); + for (ExtractResult er : ers) { + String afterStr = text.substring(er.getStart() + er.getLength()); + ConditionalMatch match = RegexExtension.matchBegin(this.config.getFollowedUnit(), afterStr, true); + if (match.getSuccess() && match.getMatch().get().index == 0) { + result.add(new Token(er.getStart(), er.getStart() + er.getLength() + match.getMatch().get().length)); + } + } + + // handle "3hrs" + result.addAll(this.getTokenFromRegex(this.config.getNumberCombinedWithUnit(), text)); + + // handle "an hour" + result.addAll(this.getTokenFromRegex(this.config.getAnUnitRegex(), text)); + + // handle "few" related cases + result.addAll(this.getTokenFromRegex(this.config.getInexactNumberUnitRegex(), text)); + + return result; + } + + private Collection getTokenFromRegex(Pattern pattern, String text) { + Collection result = new ArrayList<>(); + + for (Match match : RegExpUtility.getMatches(pattern, text)) { + result.add(new Token(match.index, match.index + match.length)); + } + + return result; + } + + // handle cases look like: {number} {unit}? and {an|a} {half|quarter} {unit}? + // define the part "and {an|a} {half|quarter}" as Suffix + private Collection numberWithUnitAndSuffix(String text, Collection tokens) { + Collection result = new ArrayList<>(); + + for (Token token : tokens) { + String afterStr = text.substring(token.getStart() + token.getLength()); + ConditionalMatch match = RegexExtension.matchBegin(this.config.getSuffixAndRegex(), afterStr, true); + if (match.getSuccess() && match.getMatch().get().index == 0) { + result.add(new Token(token.getStart(), token.getStart() + token.getLength() + match.getMatch().get().length)); + } + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseHolidayExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseHolidayExtractor.java new file mode 100644 index 000000000..d901688f1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseHolidayExtractor.java @@ -0,0 +1,60 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.Metadata; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.IHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class BaseHolidayExtractor implements IDateTimeExtractor { + + private final IHolidayExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_DATE; + } + + public BaseHolidayExtractor(IHolidayExtractorConfiguration config) { + this.config = config; + } + + @Override + public final List extract(String input, LocalDateTime reference) { + List tokens = new ArrayList<>(); + tokens.addAll(holidayMatch(input)); + List ers = Token.mergeAllTokens(tokens, input, getExtractorName()); + for (ExtractResult er : ers) { + Metadata metadata = new Metadata() { + { + setIsHoliday(true); + } + }; + er.setMetadata(metadata); + } + return ers; + } + + @Override + public final List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + private List holidayMatch(String text) { + List ret = new ArrayList<>(); + for (Pattern regex : this.config.getHolidayRegexes()) { + Match[] matches = RegExpUtility.getMatches(regex, text); + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + } + return ret; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseMergedDateTimeExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseMergedDateTimeExtractor.java new file mode 100644 index 000000000..eb765c993 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseMergedDateTimeExtractor.java @@ -0,0 +1,361 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.Metadata; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.config.IMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ProcessedSuperfluousWords; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.MatchingUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.matcher.MatchResult; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.javatuples.Pair; + +public class BaseMergedDateTimeExtractor implements IDateTimeExtractor { + + private final IMergedExtractorConfiguration config; + + @Override + public String getExtractorName() { + return ""; + } + + public BaseMergedDateTimeExtractor(IMergedExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input, LocalDateTime reference) { + + List ret = new ArrayList<>(); + String originInput = input; + Iterable> superfluousWordMatches = null; + + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + ProcessedSuperfluousWords processedSuperfluousWords = MatchingUtil.preProcessTextRemoveSuperfluousWords(input, this.config.getSuperfluousWordMatcher()); + input = processedSuperfluousWords.getText(); + superfluousWordMatches = processedSuperfluousWords.getSuperfluousWordMatches(); + } + + // The order is important, since there is a problem in merging + addTo(ret, this.config.getDateExtractor().extract(input, reference), input); + addTo(ret, this.config.getTimeExtractor().extract(input, reference), input); + addTo(ret, this.config.getDatePeriodExtractor().extract(input, reference), input); + addTo(ret, this.config.getDurationExtractor().extract(input, reference), input); + addTo(ret, this.config.getTimePeriodExtractor().extract(input, reference), input); + addTo(ret, this.config.getDateTimePeriodExtractor().extract(input, reference), input); + addTo(ret, this.config.getDateTimeExtractor().extract(input, reference), input); + addTo(ret, this.config.getSetExtractor().extract(input, reference), input); + addTo(ret, this.config.getHolidayExtractor().extract(input, reference), input); + + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + addTo(ret, this.config.getTimeZoneExtractor().extract(input, reference), input); + ret = this.config.getTimeZoneExtractor().removeAmbiguousTimezone(ret); + } + + // This should be at the end since if need the extractor to determine the previous text contains time or not + addTo(ret, numberEndingRegexMatch(input, ret), input); + + // modify time entity to an alternative DateTime expression if it follows a DateTime entity + if (this.config.getOptions().match(DateTimeOptions.ExtendedTypes)) { + ret = this.config.getDateTimeAltExtractor().extract(ret, input, reference); + } + + ret = filterUnspecificDatePeriod(ret, input); + + // Remove common ambiguous cases + ret = filterAmbiguity(ret, input); + + ret = addMod(ret, input); + + // filtering + if (this.config.getOptions().match(DateTimeOptions.CalendarMode)) { + checkCalendarFilterList(ret, input); + } + + ret.sort(Comparator.comparingInt(r -> r.getStart())); + + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + ret = MatchingUtil.posProcessExtractionRecoverSuperfluousWords(ret, superfluousWordMatches, originInput); + } + + return ret; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + private List filterAmbiguity(List extractResults, String input) { + if (config.getAmbiguityFiltersDict() != null) { + for (Pair pair : config.getAmbiguityFiltersDict()) { + final Pattern key = pair.getValue0(); + final Pattern value = pair.getValue1(); + + for (ExtractResult extractResult : extractResults) { + Optional keyMatch = Arrays.stream(RegExpUtility.getMatches(key, extractResult.getText())).findFirst(); + if (keyMatch.isPresent()) { + final Match[] matches = RegExpUtility.getMatches(value, input); + extractResults = extractResults.stream() + .filter(er -> Arrays.stream(matches).noneMatch(m -> m.index < er.getStart() + er.getLength() && m.index + m.length > er.getStart())) + .collect(Collectors.toList()); + } + } + } + } + + return extractResults; + } + + private void addTo(List dst, List src, String text) { + for (ExtractResult result : src) { + if (config.getOptions().match(DateTimeOptions.SkipFromToMerge)) { + if (shouldSkipFromToMerge(result)) { + continue; + } + } + + boolean isFound = false; + List overlapIndexes = new ArrayList<>(); + int firstIndex = -1; + for (int i = 0; i < dst.size(); i++) { + if (dst.get(i).isOverlap(result)) { + isFound = true; + if (dst.get(i).isCover(result)) { + if (firstIndex == -1) { + firstIndex = i; + } + + overlapIndexes.add(i); + } else { + break; + } + } + } + + if (!isFound) { + dst.add(result); + } else if (overlapIndexes.size() > 0) { + List tempDst = new ArrayList<>(); + for (int i = 0; i < dst.size(); i++) { + if (!overlapIndexes.contains(i)) { + tempDst.add(dst.get(i)); + } + } + + // insert at the first overlap occurrence to keep the order + tempDst.add(firstIndex, result); + dst.clear(); + dst.addAll(tempDst); + } + + dst.sort(Comparator.comparingInt(a -> a.getStart())); + } + } + + private boolean shouldSkipFromToMerge(ExtractResult er) { + return Arrays.stream(RegExpUtility.getMatches(config.getFromToRegex(), er.getText())).findFirst().isPresent(); + } + + private List numberEndingRegexMatch(String text, List extractResults) { + List tokens = new ArrayList<>(); + + for (ExtractResult extractResult : extractResults) { + if (extractResult.getType().equals(Constants.SYS_DATETIME_TIME) || extractResult.getType().equals(Constants.SYS_DATETIME_DATETIME)) { + String stringAfter = text.substring(extractResult.getStart() + extractResult.getLength()); + Pattern numberEndingPattern = this.config.getNumberEndingPattern(); + Optional match = Arrays.stream(RegExpUtility.getMatches(numberEndingPattern, stringAfter)).findFirst(); + if (match.isPresent()) { + MatchGroup newTime = match.get().getGroup("newTime"); + List numRes = this.config.getIntegerExtractor().extract(newTime.value); + if (numRes.size() == 0) { + continue; + } + + int startPosition = extractResult.getStart() + extractResult.getLength() + newTime.index; + tokens.add(new Token(startPosition, startPosition + newTime.length)); + } + + } + } + + return Token.mergeAllTokens(tokens, text, Constants.SYS_DATETIME_TIME); + } + + private List filterUnspecificDatePeriod(List ers, String text) { + ers.removeIf(er -> Arrays.stream(RegExpUtility.getMatches(config.getUnspecificDatePeriodRegex(), er.getText())).findFirst().isPresent()); + return ers; + } + + private List addMod(List ers, String text) { + int index = 0; + + for (ExtractResult er : ers.toArray(new ExtractResult[0])) { + MergeModifierResult modifiedToken = tryMergeModifierToken(er, config.getBeforeRegex(), text); + + if (!modifiedToken.result) { + modifiedToken = tryMergeModifierToken(er, config.getAfterRegex(), text); + } + + if (!modifiedToken.result) { + // SinceRegex in English contains the term "from" which is potentially ambiguous with ranges in the form "from X to Y" + modifiedToken = tryMergeModifierToken(er, config.getSinceRegex(), text, true); + } + + if (!modifiedToken.result) { + modifiedToken = tryMergeModifierToken(er, config.getAroundRegex(), text); + } + + ers.set(index, modifiedToken.er); + + final ExtractResult newEr = modifiedToken.er; + + if (newEr.getType().equals(Constants.SYS_DATETIME_DATEPERIOD) || newEr.getType().equals(Constants.SYS_DATETIME_DATE) || + newEr.getType().equals(Constants.SYS_DATETIME_TIME)) { + + // 2012 or after/above, 3 pm or later + String afterStr = text.substring(newEr.getStart() + newEr.getLength()).toLowerCase(); + + ConditionalMatch match = RegexExtension.matchBegin(config.getSuffixAfterRegex(), StringUtility.trimStart(afterStr), true); + + if (match.getSuccess()) { + boolean isFollowedByOtherEntity = true; + + if (match.getMatch().get().length == afterStr.trim().length()) { + isFollowedByOtherEntity = false; + + } else { + String nextStr = afterStr.trim().substring(match.getMatch().get().length).trim(); + ExtractResult nextEr = ers.stream().filter(t -> t.getStart() > newEr.getStart()).findFirst().orElse(null); + + if (nextEr == null || !nextStr.startsWith(nextEr.getText())) { + isFollowedByOtherEntity = false; + } + } + + if (!isFollowedByOtherEntity) { + int modLength = match.getMatch().get().length + afterStr.indexOf(match.getMatch().get().value); + int length = newEr.getLength() + modLength; + String newText = text.substring(newEr.getStart(), newEr.getStart() + length); + + er.setMetadata(assignModMetadata(er.getMetadata())); + + ers.set(index, new ExtractResult(er.getStart(), length, newText, er.getType(), er.getData(), er.getMetadata())); + } + } + } + + index++; + } + + return ers; + } + + private MergeModifierResult tryMergeModifierToken(ExtractResult er, Pattern tokenRegex, String text) { + return tryMergeModifierToken(er, tokenRegex, text, false); + } + + private MergeModifierResult tryMergeModifierToken(ExtractResult er, Pattern tokenRegex, String text, boolean potentialAmbiguity) { + String beforeStr = text.substring(0, er.getStart()).toLowerCase(); + + // Avoid adding mod for ambiguity cases, such as "from" in "from ... to ..." should not add mod + if (potentialAmbiguity && config.getAmbiguousRangeModifierPrefix() != null && + Arrays.stream(RegExpUtility.getMatches(config.getAmbiguousRangeModifierPrefix(), text)).findFirst().isPresent()) { + final Match[] matches = RegExpUtility.getMatches(config.getPotentialAmbiguousRangeRegex(), text); + if (Arrays.stream(matches).anyMatch(m -> m.index < er.getStart() + er.getLength() && m.index + m.length > er.getStart())) { + return new MergeModifierResult(false, er); + } + } + + ResultIndex result = hasTokenIndex(StringUtility.trimEnd(beforeStr), tokenRegex); + if (result.getResult()) { + int modLength = beforeStr.length() - result.getIndex(); + int length = er.getLength() + modLength; + int start = er.getStart() - modLength; + String newText = text.substring(start, start + length); + + er.setText(newText); + er.setLength(length); + er.setStart(start); + + er.setMetadata(assignModMetadata(er.getMetadata())); + + return new MergeModifierResult(true, er); + } + + return new MergeModifierResult(false, er); + } + + private Metadata assignModMetadata(Metadata metadata) { + + if (metadata == null) { + metadata = new Metadata() { + { + setHasMod(true); + } + }; + } else { + metadata.setHasMod(true); + } + + return metadata; + } + + private ResultIndex hasTokenIndex(String text, Pattern pattern) { + Match[] matches = RegExpUtility.getMatches(pattern, text); + + // Support cases has two or more specific tokens + // For example, "show me sales after 2010 and before 2018 or before 2000" + // When extract "before 2000", we need the second "before" which will be matched in the second Regex match + for (Match match : matches) { + if (StringUtility.isNullOrWhiteSpace(text.substring(match.index + match.length))) { + return new ResultIndex(true, match.index); + } + } + + return new ResultIndex(false, -1); + } + + private void checkCalendarFilterList(List ers, String text) { + List shallowCopy = new ArrayList<>(ers); + Collections.reverse(shallowCopy); + for (ExtractResult er : shallowCopy) { + for (Pattern negRegex : this.config.getFilterWordRegexList()) { + Optional match = Arrays.stream(RegExpUtility.getMatches(negRegex, er.getText())).findFirst(); + if (match.isPresent()) { + ers.remove(er); + } + } + } + } + + private class MergeModifierResult { + public final boolean result; + public final ExtractResult er; + + private MergeModifierResult(boolean result, ExtractResult er) { + this.result = result; + this.er = er; + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseSetExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseSetExtractor.java new file mode 100644 index 000000000..dbf346bd7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseSetExtractor.java @@ -0,0 +1,160 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.ISetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class BaseSetExtractor implements IDateTimeExtractor { + + private final ISetExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_SET; + } + + public BaseSetExtractor(ISetExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + @Override + public List extract(String input, LocalDateTime reference) { + List tokens = new ArrayList<>(); + tokens.addAll(matchEachUnit(input)); + tokens.addAll(timeEveryday(input, reference)); + tokens.addAll(matchEachDuration(input, reference)); + tokens.addAll(matchEach(this.config.getDateExtractor(), input, reference)); + tokens.addAll(matchEach(this.config.getTimeExtractor(), input, reference)); + tokens.addAll(matchEach(this.config.getDateTimeExtractor(), input, reference)); + tokens.addAll(matchEach(this.config.getDatePeriodExtractor(), input, reference)); + tokens.addAll(matchEach(this.config.getTimePeriodExtractor(), input, reference)); + tokens.addAll(matchEach(this.config.getDateTimePeriodExtractor(), input, reference)); + + return Token.mergeAllTokens(tokens, input, getExtractorName()); + } + + public final List matchEachUnit(String text) { + List ret = new ArrayList<>(); + + // handle "daily", "monthly" + Pattern pattern = this.config.getPeriodicRegex(); + Match[] matches = RegExpUtility.getMatches(pattern, text); + + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + + // handle "each month" + pattern = this.config.getEachUnitRegex(); + matches = RegExpUtility.getMatches(pattern, text); + + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + + return ret; + } + + public final List matchEachDuration(String text, LocalDateTime reference) { + List ret = new ArrayList<>(); + List ers = this.config.getDurationExtractor().extract(text, reference); + + for (ExtractResult er : ers) { + // "each last summer" doesn't make sense + Pattern lastRegex = this.config.getLastRegex(); + if (RegExpUtility.getMatches(lastRegex, er.getText()).length > 0) { + continue; + } + + String beforeStr = text.substring(0, (er.getStart() != null) ? er.getStart() : 0); + Pattern eachPrefixRegex = this.config.getEachPrefixRegex(); + Optional match = Arrays.stream(RegExpUtility.getMatches(eachPrefixRegex, beforeStr)).findFirst(); + if (match.isPresent()) { + ret.add(new Token(match.get().index, er.getStart() + er.getLength())); + } + } + return ret; + } + + public final List timeEveryday(String text, LocalDateTime reference) { + List ret = new ArrayList<>(); + List ers = this.config.getTimeExtractor().extract(text, reference); + for (ExtractResult er : ers) { + String afterStr = text.substring(er.getStart() + er.getLength()); + if (StringUtility.isNullOrEmpty(afterStr) && this.config.getBeforeEachDayRegex() != null) { + String beforeStr = text.substring(0, er.getStart()); + Pattern beforeEachDayRegex = this.config.getBeforeEachDayRegex(); + Optional match = Arrays.stream(RegExpUtility.getMatches(beforeEachDayRegex, beforeStr)).findFirst(); + if (match.isPresent()) { + ret.add(new Token(match.get().index, er.getStart() + er.getLength())); + } + } else { + Pattern eachDayRegex = this.config.getEachDayRegex(); + Optional match = Arrays.stream(RegExpUtility.getMatches(eachDayRegex, afterStr)).findFirst(); + if (match.isPresent()) { + ret.add(new Token(er.getStart(), er.getStart() + er.getLength() + match.get().length)); + } + } + } + + return ret; + } + + public final List matchEach(IDateTimeExtractor extractor, String input, LocalDateTime reference) { + StringBuilder sb = new StringBuilder(input); + List ret = new ArrayList<>(); + Pattern setEachRegex = this.config.getSetEachRegex(); + Match[] matches = RegExpUtility.getMatches(setEachRegex, input); + for (Match match : matches) { + if (match != null) { + String trimedText = sb.delete(match.index, match.index + match.length).toString(); + List ers = extractor.extract(trimedText, reference); + for (ExtractResult er : ers) { + if (er.getStart() <= match.index && (er.getStart() + er.getLength()) > match.index) { + ret.add(new Token(er.getStart(), er.getStart() + er.getLength() + match.length)); + } + } + } + } + + // handle "Mondays" + Pattern setWeekDayRegex = this.config.getSetWeekDayRegex(); + matches = RegExpUtility.getMatches(setWeekDayRegex, input); + for (Match match : matches) { + if (match != null) { + sb = new StringBuilder(input); + sb.delete(match.index, match.index + match.length); + String trimedText = sb.insert(match.index, match.getGroup("weekday").value).toString(); + + List ers = extractor.extract(trimedText, reference); + for (ExtractResult er : ers) { + if (er.getStart() <= match.index && er.getText().contains(match.getGroup("weekday").value)) { + int len = er.getLength() + 1; + if (match.getGroup(Constants.PrefixGroupName).value != "") { + len += match.getGroup(Constants.PrefixGroupName).value.length(); + } + ret.add(new Token(er.getStart(), er.getStart() + len)); + } + } + } + } + + return ret; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimeExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimeExtractor.java new file mode 100644 index 000000000..75b9547a6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimeExtractor.java @@ -0,0 +1,165 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class BaseTimeExtractor implements IDateTimeExtractor { + + private final ITimeExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_TIME; + } + + public BaseTimeExtractor(ITimeExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input, LocalDateTime reference) { + + List tokens = new ArrayList<>(); + tokens.addAll(basicRegexMatch(input)); + tokens.addAll(atRegexMatch(input)); + tokens.addAll(beforeAfterRegexMatch(input)); + tokens.addAll(specialCasesRegexMatch(input)); + + List timeErs = Token.mergeAllTokens(tokens, input, getExtractorName()); + + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + timeErs = mergeTimeZones(timeErs, config.getTimeZoneExtractor().extract(input, reference), input); + } + + return timeErs; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + private List mergeTimeZones(List timeErs, List timeZoneErs, String text) { + int erIndex = 0; + for (ExtractResult er : timeErs.toArray(new ExtractResult[0])) { + for (ExtractResult timeZoneEr : timeZoneErs) { + int begin = er.getStart() + er.getLength(); + int end = timeZoneEr.getStart(); + + if (begin < end) { + String gapText = text.substring(begin, end); + + if (StringUtility.isNullOrWhiteSpace(gapText)) { + int newLenght = timeZoneEr.getStart() + timeZoneEr.getLength(); + String newText = text.substring(er.getStart(), newLenght); + Map newData = new HashMap<>(); + newData.put(Constants.SYS_DATETIME_TIMEZONE, timeZoneEr); + + er.setData(newData); + er.setText(newText); + er.setLength(newLenght - er.getStart()); + timeErs.set(erIndex, er); + } + } + } + erIndex++; + } + return timeErs; + } + + public final List basicRegexMatch(String text) { + + List ret = new ArrayList<>(); + + for (Pattern regex : this.config.getTimeRegexList()) { + + Match[] matches = RegExpUtility.getMatches(regex, text); + for (Match match : matches) { + + // @TODO Remove when lookbehinds are handled correctly + if (isDecimal(match, text)) { + continue; + } + + // @TODO Workaround to avoid incorrect partial-only matches. Remove after time regex reviews across languages. + String lth = match.getGroup("lth").value; + + if ((lth == null || lth.length() == 0) || + (lth.length() != match.length && !(match.length == lth.length() + 1 && match.value.endsWith(" ")))) { + + ret.add(new Token(match.index, match.index + match.length)); + } + } + } + + return ret; + } + + // Check if the match is part of a decimal number (e.g. 123.24) + private boolean isDecimal(Match match, String text) { + boolean isDecimal = false; + if (match.index > 1 && (text.charAt(match.index - 1) == ',' || + text.charAt(match.index - 1) == '.') && Character.isDigit(text.charAt(match.index - 2)) && Character.isDigit(match.value.charAt(0))) { + isDecimal = true; + } + + return isDecimal; + } + + private List atRegexMatch(String text) { + List ret = new ArrayList<>(); + // handle "at 5", "at seven" + Pattern pattern = this.config.getAtRegex(); + Match[] matches = RegExpUtility.getMatches(pattern, text); + if (matches.length > 0) { + for (Match match : matches) { + if (match.index + match.length < text.length() && text.charAt(match.index + match.length) == '%') { + continue; + } + ret.add(new Token(match.index, match.index + match.length)); + } + } + return ret; + } + + private List beforeAfterRegexMatch(String text) { + List ret = new ArrayList<>(); + // only enabled in CalendarMode + if (this.config.getOptions().match(DateTimeOptions.CalendarMode)) { + // handle "before 3", "after three" + Pattern beforeAfterRegex = this.config.getTimeBeforeAfterRegex(); + Match[] matches = RegExpUtility.getMatches(beforeAfterRegex, text); + if (matches.length > 0) { + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + } + } + return ret; + } + + private List specialCasesRegexMatch(String text) { + List ret = new ArrayList<>(); + // handle "ish" + if (this.config.getIshRegex() != null && RegExpUtility.getMatches(this.config.getIshRegex(), text).length > 0) { + Match[] matches = RegExpUtility.getMatches(this.config.getIshRegex(), text); + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + } + return ret; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimePeriodExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimePeriodExtractor.java new file mode 100644 index 000000000..d1ee998ec --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimePeriodExtractor.java @@ -0,0 +1,275 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneUtility; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class BaseTimePeriodExtractor implements IDateTimeExtractor { + + private final ITimePeriodExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_TIMEPERIOD; + } + + public BaseTimePeriodExtractor(ITimePeriodExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input, LocalDateTime reference) { + + List tokens = new ArrayList<>(); + tokens.addAll(matchSimpleCases(input)); + tokens.addAll(mergeTwoTimePoints(input, reference)); + tokens.addAll(matchTimeOfDay(input)); + + List timePeriodErs = Token.mergeAllTokens(tokens, input, getExtractorName()); + + if (config.getOptions().match(DateTimeOptions.EnablePreview)) { + timePeriodErs = TimeZoneUtility.mergeTimeZones(timePeriodErs, config.getTimeZoneExtractor().extract(input, reference), input); + } + + return timePeriodErs; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + // Cases like "from 3 to 5am" or "between 3:30 and 5" are extracted here + // Note that cases like "from 3 to 5" will not be extracted here because no "am/pm" or "hh:mm" to infer it's a time period + // Also cases like "from 3:30 to 4 people" should not be extracted as a time period + private List matchSimpleCases(String input) { + + List ret = new ArrayList<>(); + + for (Pattern regex : this.config.getSimpleCasesRegex()) { + Match[] matches = RegExpUtility.getMatches(regex, input); + + for (Match match : matches) { + // Cases like "from 10:30 to 11", don't necessarily need "am/pm" + if (!match.getGroup(Constants.MinuteGroupName).value.equals("") || !match.getGroup(Constants.SecondGroupName).value.equals("")) { + // Cases like "from 3:30 to 4" should be supported + // Cases like "from 3:30 to 4 on 1/1/2015" should be supported + // Cases like "from 3:30 to 4 people" is considered not valid + Boolean endWithValidToken = false; + + // "No extra tokens after the time period" + if (match.index + match.length == input.length()) { + endWithValidToken = true; + } else { + String afterStr = input.substring(match.index + match.length); + + // "End with general ending tokens or "TokenBeforeDate" (like "on") + boolean endWithGeneralEndings = Arrays.stream(RegExpUtility.getMatches(this.config.getGeneralEndingRegex(), afterStr)) + .findFirst().isPresent(); + boolean endWithAmPm = !match.getGroup(Constants.RightAmPmGroupName).value.equals(""); + if (endWithGeneralEndings || endWithAmPm || afterStr.trim().startsWith(this.config.getTokenBeforeDate())) { + endWithValidToken = true; + } else if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + endWithValidToken = startsWithTimeZone(afterStr); + } + } + + if (endWithValidToken) { + ret.add(new Token(match.index, match.index + match.length)); + } + } else { + // Is there Constants.PmGroupName or Constants.AmGroupName ? + String pmStr = match.getGroup(Constants.PmGroupName).value; + String amStr = match.getGroup(Constants.AmGroupName).value; + String descStr = match.getGroup(Constants.DescGroupName).value; + + // Check "pm", "am" + if (!StringUtility.isNullOrEmpty(pmStr) || !StringUtility.isNullOrEmpty(amStr) || !StringUtility.isNullOrEmpty(descStr)) { + ret.add(new Token(match.index, match.index + match.length)); + } else { + String afterStr = input.substring(match.index + match.length); + + if ((this.config.getOptions().match(DateTimeOptions.EnablePreview)) && startsWithTimeZone(afterStr)) { + ret.add(new Token(match.index, match.index + match.length)); + } + } + } + } + } + + return ret; + } + + private boolean startsWithTimeZone(String afterText) { + boolean startsWithTimeZone = false; + + List timeZoneErs = config.getTimeZoneExtractor().extract(afterText); + Optional firstTimeZone = timeZoneErs.stream().sorted(Comparator.comparingInt(t -> t.getStart())).findFirst(); + + if (firstTimeZone.isPresent()) { + String beforeText = afterText.substring(0, firstTimeZone.get().getStart()); + + if (StringUtility.isNullOrWhiteSpace(beforeText)) { + startsWithTimeZone = true; + } + } + + return startsWithTimeZone; + } + + private List mergeTwoTimePoints(String input, LocalDateTime reference) { + + List ret = new ArrayList<>(); + List ers = this.config.getSingleTimeExtractor().extract(input, reference); + + // Handling ending number as a time point. + List numErs = this.config.getIntegerExtractor().extract(input); + + // Check if it is an ending number + if (numErs.size() > 0) { + List timeNumbers = new ArrayList<>(); + + // check if it is a ending number + boolean endingNumber = false; + ExtractResult num = numErs.get(numErs.size() - 1); + if (num.getStart() + num.getLength() == input.length()) { + endingNumber = true; + } else { + String afterStr = input.substring(num.getStart() + num.getLength()); + Pattern generalEndingRegex = this.config.getGeneralEndingRegex(); + Optional endingMatch = Arrays.stream(RegExpUtility.getMatches(generalEndingRegex, input)).findFirst(); + if (endingMatch.isPresent()) { + endingNumber = true; + } + } + if (endingNumber) { + timeNumbers.add(num); + } + + int i = 0; + int j = 0; + + while (i < numErs.size()) { + // find subsequent time point + int numEndPoint = numErs.get(i).getStart() + numErs.get(i).getLength(); + while (j < ers.size() && ers.get(j).getStart() <= numEndPoint) { + j++; + } + + if (j >= ers.size()) { + break; + } + + // check connector string + String midStr = input.substring(numEndPoint, ers.get(j).getStart()); + Pattern tillRegex = this.config.getTillRegex(); + if (RegexExtension.isExactMatch(tillRegex, midStr, true) || config.hasConnectorToken(midStr.trim())) { + timeNumbers.add(numErs.get(i)); + } + + i++; + } + + // check overlap + for (ExtractResult timeNum : timeNumbers) { + boolean overlap = false; + for (ExtractResult er : ers) { + if (er.getStart() <= timeNum.getStart() && er.getStart() + er.getLength() >= timeNum.getStart()) { + overlap = true; + } + } + + if (!overlap) { + ers.add(timeNum); + } + } + + ers.sort((x, y) -> x.getStart() - y.getStart()); + } + + int idx = 0; + while (idx < ers.size() - 1) { + int middleBegin = ers.get(idx).getStart() + ers.get(idx).getLength(); + int middleEnd = ers.get(idx + 1).getStart(); + + if (middleEnd - middleBegin <= 0) { + idx++; + continue; + } + + String middleStr = input.substring(middleBegin, middleEnd).trim().toLowerCase(java.util.Locale.ROOT); + Pattern tillRegex = this.config.getTillRegex(); + + // Handle "{TimePoint} to {TimePoint}" + if (RegexExtension.isExactMatch(tillRegex, middleStr, true)) { + int periodBegin = ers.get(idx).getStart(); + int periodEnd = ers.get(idx + 1).getStart() + ers.get(idx + 1).getLength(); + + // Handle "from" + String beforeStr = StringUtility.trimEnd(input.substring(0, periodBegin)).toLowerCase(); + ResultIndex fromIndex = this.config.getFromTokenIndex(beforeStr); + ResultIndex betweenIndex = this.config.getBetweenTokenIndex(beforeStr); + if (fromIndex.getResult()) { + // Handle "from" + periodBegin = fromIndex.getIndex(); + } else if (betweenIndex.getResult()) { + // Handle "between" + periodBegin = betweenIndex.getIndex(); + } + + ret.add(new Token(periodBegin, periodEnd)); + idx += 2; + continue; + } + + // Handle "between {TimePoint} and {TimePoint}" + if (this.config.hasConnectorToken(middleStr)) { + int periodBegin = ers.get(idx).getStart(); + int periodEnd = ers.get(idx + 1).getStart() + ers.get(idx + 1).getLength(); + + // Handle "between" + String beforeStr = input.substring(0, periodBegin).trim().toLowerCase(java.util.Locale.ROOT); + ResultIndex betweenIndex = this.config.getBetweenTokenIndex(beforeStr); + if (betweenIndex.getResult()) { + periodBegin = betweenIndex.getIndex(); + ret.add(new Token(periodBegin, periodEnd)); + idx += 2; + continue; + } + } + + idx++; + } + + return ret; + } + + private List matchTimeOfDay(String input) { + + List ret = new ArrayList<>(); + Pattern timeOfDayRegex = this.config.getTimeOfDayRegex(); + Match[] matches = RegExpUtility.getMatches(timeOfDayRegex, input); + for (Match match : matches) { + ret.add(new Token(match.index, match.index + match.length)); + } + + return ret; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimeZoneExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimeZoneExtractor.java new file mode 100644 index 000000000..e1401d428 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/BaseTimeZoneExtractor.java @@ -0,0 +1,133 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeZoneExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.MatchingUtil; +import com.microsoft.recognizers.text.datetime.utilities.Token; +import com.microsoft.recognizers.text.matcher.MatchResult; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.QueryProcessor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class BaseTimeZoneExtractor implements IDateTimeZoneExtractor { + + private final ITimeZoneExtractorConfiguration config; + + @Override + public String getExtractorName() { + return Constants.SYS_DATETIME_TIMEZONE; + } + + public BaseTimeZoneExtractor(ITimeZoneExtractorConfiguration config) { + this.config = config; + } + + @Override + public List extract(String input) { + return this.extract(input, LocalDateTime.now()); + } + + @Override + public List extract(String input, LocalDateTime reference) { + String normalizedText = QueryProcessor.removeDiacritics(input); + List tokens = new ArrayList<>(); + tokens.addAll(matchTimeZones(normalizedText)); + tokens.addAll(matchLocationTimes(normalizedText, tokens)); + return Token.mergeAllTokens(tokens, input, getExtractorName()); + } + + @Override + public List removeAmbiguousTimezone(List extractResults) { + return extractResults.stream().filter(o -> !config.getAmbiguousTimezoneList().contains(o.getText().toLowerCase())).collect(Collectors.toList()); + } + + private List matchLocationTimes(String text, List tokens) { + List ret = new ArrayList<>(); + + if (config.getLocationTimeSuffixRegex() == null) { + return ret; + } + + Match[] timeMatch = RegExpUtility.getMatches(config.getLocationTimeSuffixRegex(), text); + + // Before calling a Find() in location matcher, check if all the matched suffixes by + // LocationTimeSuffixRegex are already inside tokens extracted by TimeZone matcher. + // If so, don't call the Find() as they have been extracted by TimeZone matcher, otherwise, call it. + + boolean isAllSuffixInsideTokens = true; + + for (Match match : timeMatch) { + boolean isInside = false; + for (Token token : tokens) { + if (token.getStart() <= match.index && token.getEnd() >= match.index + match.length) { + isInside = true; + break; + } + } + + if (!isInside) { + isAllSuffixInsideTokens = false; + } + + if (!isAllSuffixInsideTokens) { + break; + } + } + + if (timeMatch.length != 0 && !isAllSuffixInsideTokens) { + int lastMatchIndex = timeMatch[timeMatch.length - 1].index; + Iterable> matches = config.getLocationMatcher().find(text.substring(0, lastMatchIndex).toLowerCase()); + List> locationMatches = MatchingUtil.removeSubMatches(matches); + + int i = 0; + for (Match match : timeMatch) { + boolean hasCityBefore = false; + + while (i < locationMatches.size() && locationMatches.get(i).getEnd() <= match.index) { + hasCityBefore = true; + i++; + + if (i == locationMatches.size()) { + break; + } + } + + if (hasCityBefore && locationMatches.get(i - 1).getEnd() == match.index) { + ret.add(new Token(locationMatches.get(i - 1).getStart(), match.index + match.length)); + } + + if (i == locationMatches.size()) { + break; + } + } + } + + return ret; + } + + private List matchTimeZones(String text) { + List ret = new ArrayList<>(); + + // Direct UTC matches + Match[] directUtcMatches = RegExpUtility.getMatches(config.getDirectUtcRegex(), text.toLowerCase()); + if (directUtcMatches.length > 0) { + for (Match match : directUtcMatches) { + ret.add(new Token(match.index, match.index + match.length)); + } + } + + Iterable> matches = config.getTimeZoneMatcher().find(text.toLowerCase()); + for (MatchResult match : matches) { + ret.add(new Token(match.getStart(), match.getStart() + match.getLength())); + } + + return ret; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateExtractor.java new file mode 100644 index 000000000..78d19f42e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateExtractor.java @@ -0,0 +1,7 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.utilities.Match; + +public interface IDateExtractor extends IDateTimeExtractor { + int getYearFromText(Match match); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeExtractor.java new file mode 100644 index 000000000..f21f49c41 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeExtractor.java @@ -0,0 +1,13 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IExtractor; + +import java.time.LocalDateTime; +import java.util.List; + +public interface IDateTimeExtractor extends IExtractor { + String getExtractorName(); + + List extract(String input, LocalDateTime reference); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeListExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeListExtractor.java new file mode 100644 index 000000000..4d7ab6d5c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeListExtractor.java @@ -0,0 +1,12 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; + +import java.time.LocalDateTime; +import java.util.List; + +public interface IDateTimeListExtractor { + String getExtractorName(); + + List extract(List extractResults, String text, LocalDateTime reference); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeZoneExtractor.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeZoneExtractor.java new file mode 100644 index 000000000..8b9b1a151 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/IDateTimeZoneExtractor.java @@ -0,0 +1,9 @@ +package com.microsoft.recognizers.text.datetime.extractors; + +import com.microsoft.recognizers.text.ExtractResult; + +import java.util.List; + +public interface IDateTimeZoneExtractor extends IDateTimeExtractor { + List removeAmbiguousTimezone(List extractResults); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateExtractorConfiguration.java new file mode 100644 index 000000000..4498324bb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateExtractorConfiguration.java @@ -0,0 +1,62 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.regex.Pattern; + +public interface IDateExtractorConfiguration extends IOptionsConfiguration { + Iterable getDateRegexList(); + + Iterable getImplicitDateList(); + + Pattern getOfMonth(); + + Pattern getMonthEnd(); + + Pattern getWeekDayEnd(); + + Pattern getDateUnitRegex(); + + Pattern getForTheRegex(); + + Pattern getWeekDayAndDayOfMonthRegex(); + + Pattern getRelativeMonthRegex(); + + Pattern getStrictRelativeRegex(); + + Pattern getWeekDayRegex(); + + Pattern getPrefixArticleRegex(); + + Pattern getYearSuffix(); + + Pattern getMoreThanRegex(); + + Pattern getLessThanRegex(); + + Pattern getInConnectorRegex(); + + Pattern getRangeUnitRegex(); + + Pattern getRangeConnectorSymbolRegex(); + + IExtractor getIntegerExtractor(); + + IExtractor getOrdinalExtractor(); + + IParser getNumberParser(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); + + ImmutableMap getDayOfWeek(); + + ImmutableMap getMonthOfYear(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDatePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDatePeriodExtractorConfiguration.java new file mode 100644 index 000000000..6c7996691 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDatePeriodExtractorConfiguration.java @@ -0,0 +1,79 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; + +import java.util.regex.Pattern; + +public interface IDatePeriodExtractorConfiguration { + Iterable getSimpleCasesRegexes(); + + Pattern getIllegalYearRegex(); + + Pattern getYearRegex(); + + Pattern getTillRegex(); + + Pattern getDateUnitRegex(); + + Pattern getTimeUnitRegex(); + + Pattern getFollowedDateUnit(); + + Pattern getNumberCombinedWithDateUnit(); + + Pattern getPastRegex(); + + Pattern getFutureRegex(); + + Pattern getFutureSuffixRegex(); + + Pattern getWeekOfRegex(); + + Pattern getMonthOfRegex(); + + Pattern getRangeUnitRegex(); + + Pattern getInConnectorRegex(); + + Pattern getWithinNextPrefixRegex(); + + Pattern getYearPeriodRegex(); + + Pattern getRelativeDecadeRegex(); + + Pattern getComplexDatePeriodRegex(); + + Pattern getReferenceDatePeriodRegex(); + + Pattern getAgoRegex(); + + Pattern getLaterRegex(); + + Pattern getLessThanRegex(); + + Pattern getMoreThanRegex(); + + Pattern getCenturySuffixRegex(); + + Pattern getNowRegex(); + + IDateTimeExtractor getDatePointExtractor(); + + IExtractor getCardinalExtractor(); + + IExtractor getOrdinalExtractor(); + + IDateTimeExtractor getDurationExtractor(); + + IParser getNumberParser(); + + ResultIndex getFromTokenIndex(String text); + + boolean hasConnectorToken(String text); + + ResultIndex getBetweenTokenIndex(String text); + + String[] getDurationDateRestrictions(); +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimeAltExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimeAltExtractorConfiguration.java new file mode 100644 index 000000000..8bddc5321 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimeAltExtractorConfiguration.java @@ -0,0 +1,24 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; + +import java.util.regex.Pattern; + +public interface IDateTimeAltExtractorConfiguration { + IDateExtractor getDateExtractor(); + + IDateTimeExtractor getDatePeriodExtractor(); + + Iterable getRelativePrefixList(); + + Iterable getAmPmRegexList(); + + Pattern getOrRegex(); + + Pattern getThisPrefixRegex(); + + Pattern getDayRegex(); + + Pattern getRangePrefixRegex(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimeExtractorConfiguration.java new file mode 100644 index 000000000..e26f83509 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimeExtractorConfiguration.java @@ -0,0 +1,48 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.regex.Pattern; + +public interface IDateTimeExtractorConfiguration extends IOptionsConfiguration { + Pattern getNowRegex(); + + Pattern getSuffixRegex(); + + Pattern getTimeOfTodayAfterRegex(); + + Pattern getSimpleTimeOfTodayAfterRegex(); + + Pattern getTimeOfTodayBeforeRegex(); + + Pattern getSimpleTimeOfTodayBeforeRegex(); + + Pattern getTimeOfDayRegex(); + + Pattern getSpecificEndOfRegex(); + + Pattern getUnspecificEndOfRegex(); + + Pattern getUnitRegex(); + + Pattern getNumberAsTimeRegex(); + + Pattern getDateNumberConnectorRegex(); + + Pattern getSuffixAfterRegex(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeExtractor getDatePointExtractor(); + + IDateTimeExtractor getTimePointExtractor(); + + IExtractor getIntegerExtractor(); + + boolean isConnector(String text); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..b0cc61ad7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDateTimePeriodExtractorConfiguration.java @@ -0,0 +1,81 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; + +import java.util.regex.Pattern; + +public interface IDateTimePeriodExtractorConfiguration extends IOptionsConfiguration { + String getTokenBeforeDate(); + + Iterable getSimpleCasesRegex(); + + Pattern getPrepositionRegex(); + + Pattern getTillRegex(); + + Pattern getSpecificTimeOfDayRegex(); + + Pattern getTimeOfDayRegex(); + + Pattern getFollowedUnit(); + + Pattern getNumberCombinedWithUnit(); + + Pattern getTimeUnitRegex(); + + Pattern getPastPrefixRegex(); + + Pattern getNextPrefixRegex(); + + Pattern getFutureSuffixRegex(); + + Pattern getWeekDayRegex(); + + Pattern getPeriodTimeOfDayWithDateRegex(); + + Pattern getRelativeTimeUnitRegex(); + + Pattern getRestOfDateTimeRegex(); + + Pattern getGeneralEndingRegex(); + + Pattern getMiddlePauseRegex(); + + Pattern getAmDescRegex(); + + Pattern getPmDescRegex(); + + Pattern getWithinNextPrefixRegex(); + + Pattern getDateUnitRegex(); + + Pattern getPrefixDayRegex(); + + Pattern getSuffixRegex(); + + Pattern getBeforeRegex(); + + Pattern getAfterRegex(); + + IExtractor getCardinalExtractor(); + + IDateTimeExtractor getSingleDateExtractor(); + + IDateTimeExtractor getSingleTimeExtractor(); + + IDateTimeExtractor getSingleDateTimeExtractor(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeExtractor getTimePeriodExtractor(); + + IDateTimeExtractor getTimeZoneExtractor(); + + ResultIndex getFromTokenIndex(String text); + + boolean hasConnectorToken(String text); + + ResultIndex getBetweenTokenIndex(String text); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDurationExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDurationExtractorConfiguration.java new file mode 100644 index 000000000..b68aee159 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IDurationExtractorConfiguration.java @@ -0,0 +1,45 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; + +import java.util.regex.Pattern; + +public interface IDurationExtractorConfiguration extends IOptionsConfiguration { + Pattern getFollowedUnit(); + + Pattern getNumberCombinedWithUnit(); + + Pattern getAnUnitRegex(); + + Pattern getDuringRegex(); + + Pattern getAllRegex(); + + Pattern getHalfRegex(); + + Pattern getSuffixAndRegex(); + + Pattern getConjunctionRegex(); + + Pattern getInexactNumberRegex(); + + Pattern getInexactNumberUnitRegex(); + + Pattern getRelativeDurationUnitRegex(); + + Pattern getDurationUnitRegex(); + + Pattern getDurationConnectorRegex(); + + Pattern getLessThanRegex(); + + Pattern getMoreThanRegex(); + + IExtractor getCardinalExtractor(); + + ImmutableMap getUnitMap(); + + ImmutableMap getUnitValueMap(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IHolidayExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IHolidayExtractorConfiguration.java new file mode 100644 index 000000000..f071ed03f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IHolidayExtractorConfiguration.java @@ -0,0 +1,7 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import java.util.regex.Pattern; + +public interface IHolidayExtractorConfiguration { + Iterable getHolidayRegexes(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IMergedExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IMergedExtractorConfiguration.java new file mode 100644 index 000000000..ad1c14367 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/IMergedExtractorConfiguration.java @@ -0,0 +1,68 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeListExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeZoneExtractor; +import com.microsoft.recognizers.text.matcher.StringMatcher; + +import java.util.regex.Pattern; + +import org.javatuples.Pair; + +public interface IMergedExtractorConfiguration extends IOptionsConfiguration { + IDateTimeExtractor getDateExtractor(); + + IDateTimeExtractor getTimeExtractor(); + + IDateTimeExtractor getDateTimeExtractor(); + + IDateTimeExtractor getDatePeriodExtractor(); + + IDateTimeExtractor getTimePeriodExtractor(); + + IDateTimeExtractor getDateTimePeriodExtractor(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeExtractor getSetExtractor(); + + IDateTimeExtractor getHolidayExtractor(); + + IDateTimeZoneExtractor getTimeZoneExtractor(); + + IDateTimeListExtractor getDateTimeAltExtractor(); + + IExtractor getIntegerExtractor(); + + Iterable getFilterWordRegexList(); + + Pattern getAfterRegex(); + + Pattern getBeforeRegex(); + + Pattern getSinceRegex(); + + Pattern getAroundRegex(); + + Pattern getFromToRegex(); + + Pattern getSingleAmbiguousMonthRegex(); + + Pattern getAmbiguousRangeModifierPrefix(); + + Pattern getPotentialAmbiguousRangeRegex(); + + Pattern getPrepositionSuffixRegex(); + + Pattern getNumberEndingPattern(); + + Pattern getSuffixAfterRegex(); + + Pattern getUnspecificDatePeriodRegex(); + + StringMatcher getSuperfluousWordMatcher(); + + Iterable> getAmbiguityFiltersDict(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ISetExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ISetExtractorConfiguration.java new file mode 100644 index 000000000..35620d82a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ISetExtractorConfiguration.java @@ -0,0 +1,38 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; + +import java.util.regex.Pattern; + +public interface ISetExtractorConfiguration extends IOptionsConfiguration { + Pattern getLastRegex(); + + Pattern getEachDayRegex(); + + Pattern getSetEachRegex(); + + Pattern getPeriodicRegex(); + + Pattern getEachUnitRegex(); + + Pattern getEachPrefixRegex(); + + Pattern getSetWeekDayRegex(); + + Pattern getBeforeEachDayRegex(); + + IDateTimeExtractor getTimeExtractor(); + + IDateTimeExtractor getDateExtractor(); + + IDateTimeExtractor getDateTimeExtractor(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeExtractor getDatePeriodExtractor(); + + IDateTimeExtractor getTimePeriodExtractor(); + + IDateTimeExtractor getDateTimePeriodExtractor(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimeExtractorConfiguration.java new file mode 100644 index 000000000..9c025d24e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimeExtractorConfiguration.java @@ -0,0 +1,18 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; + +import java.util.regex.Pattern; + +public interface ITimeExtractorConfiguration extends IOptionsConfiguration { + IDateTimeExtractor getTimeZoneExtractor(); + + Iterable getTimeRegexList(); + + Pattern getAtRegex(); + + Pattern getIshRegex(); + + Pattern getTimeBeforeAfterRegex(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..9f0a0cbf9 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimePeriodExtractorConfiguration.java @@ -0,0 +1,31 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; + +import java.util.regex.Pattern; + +public interface ITimePeriodExtractorConfiguration extends IOptionsConfiguration { + String getTokenBeforeDate(); + + IExtractor getIntegerExtractor(); + + Iterable getSimpleCasesRegex(); + + Pattern getTillRegex(); + + Pattern getTimeOfDayRegex(); + + Pattern getGeneralEndingRegex(); + + IDateTimeExtractor getSingleTimeExtractor(); + + ResultIndex getFromTokenIndex(String text); + + boolean hasConnectorToken(String text); + + ResultIndex getBetweenTokenIndex(String text); + + IDateTimeExtractor getTimeZoneExtractor(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimeZoneExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimeZoneExtractorConfiguration.java new file mode 100644 index 000000000..ce877754c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ITimeZoneExtractorConfiguration.java @@ -0,0 +1,19 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.matcher.StringMatcher; + +import java.util.List; +import java.util.regex.Pattern; + +public interface ITimeZoneExtractorConfiguration extends IOptionsConfiguration { + Pattern getDirectUtcRegex(); + + Pattern getLocationTimeSuffixRegex(); + + StringMatcher getLocationMatcher(); + + StringMatcher getTimeZoneMatcher(); + + List getAmbiguousTimezoneList(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ProcessedSuperfluousWords.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ProcessedSuperfluousWords.java new file mode 100644 index 000000000..42e59194e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ProcessedSuperfluousWords.java @@ -0,0 +1,23 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +import com.microsoft.recognizers.text.matcher.MatchResult; + +import java.util.List; + +public class ProcessedSuperfluousWords { + private String text; + private Iterable> superfluousWordMatches; + + public ProcessedSuperfluousWords(String text, Iterable> superfluousWordMatches) { + this.text = text; + this.superfluousWordMatches = superfluousWordMatches; + } + + public String getText() { + return text; + } + + public Iterable> getSuperfluousWordMatches() { + return superfluousWordMatches; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ResultIndex.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ResultIndex.java new file mode 100644 index 000000000..b9d90d992 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ResultIndex.java @@ -0,0 +1,27 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +public class ResultIndex { + private boolean result; + private int index; + + public ResultIndex(boolean result, int index) { + this.result = result; + this.index = index; + } + + public boolean getResult() { + return result; + } + + public int getIndex() { + return index; + } + + public void setResult(boolean result) { + this.result = result; + } + + public void setIndex(int index) { + this.index = index; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ResultTimex.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ResultTimex.java new file mode 100644 index 000000000..eaff0b5b3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/extractors/config/ResultTimex.java @@ -0,0 +1,27 @@ +package com.microsoft.recognizers.text.datetime.extractors.config; + +public class ResultTimex { + private boolean result; + private String timex; + + public ResultTimex(boolean result, String timex) { + this.result = result; + this.timex = timex; + } + + public boolean getResult() { + return result; + } + + public String getTimex() { + return timex; + } + + public void setResult(boolean result) { + this.result = result; + } + + public void setTimex(String timex) { + this.timex = timex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateExtractorConfiguration.java new file mode 100644 index 000000000..a90a1f5e9 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateExtractorConfiguration.java @@ -0,0 +1,245 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.utilities.FrenchDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.french.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.number.french.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.french.parsers.FrenchNumberParserConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class FrenchDateExtractorConfiguration extends BaseOptionsConfiguration implements IDateExtractorConfiguration { + + public static final Pattern MonthRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthRegex); + public static final Pattern MonthNumRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthNumRegex); + public static final Pattern YearRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.YearRegex); + public static final Pattern WeekDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekDayRegex); + public static final Pattern SingleWeekDayRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.SingleAmbiguousMonthRegex); + public static final Pattern OnRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.OnRegex); + public static final Pattern RelaxedOnRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelaxedOnRegex); + public static final Pattern ThisRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ThisRegex); + public static final Pattern LastDateRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.LastDateRegex); + public static final Pattern NextDateRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NextDateRegex); + public static final Pattern StrictWeekDay = RegExpUtility.getSafeRegExp(FrenchDateTime.StrictWeekDay); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DateUnitRegex); + public static final Pattern SpecialDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SpecialDayRegex); + public static final Pattern WeekDayOfMonthRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekDayOfMonthRegex); + public static final Pattern RelativeWeekDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeWeekDayRegex); + public static final Pattern SpecialDate = RegExpUtility.getSafeRegExp(FrenchDateTime.SpecialDate); + public static final Pattern SpecialDayWithNumRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.SpecialDayWithNumRegex); + public static final Pattern ForTheRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ForTheRegex); + public static final Pattern WeekDayAndDayOfMonthRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.WeekDayAndDayOfMonthRegex); + public static final Pattern RelativeMonthRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeMonthRegex); + public static final Pattern StrictRelativeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.StrictRelativeRegex); + public static final Pattern PrefixArticleRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PrefixArticleRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.InConnectorRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangeUnitRegex); + public static final Pattern RangeConnectorSymbolRegex = RegExpUtility + .getSafeRegExp(BaseDateTime.RangeConnectorSymbolRegex); + + public static final List DateRegexList = new ArrayList() { + { + add(RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor1)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor2)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor3)); + add(FrenchDateTime.DefaultLanguageFallback == "DMY" ? + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor5) : + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor4)); + add(FrenchDateTime.DefaultLanguageFallback == "DMY" ? + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor4) : + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor5)); + add(FrenchDateTime.DefaultLanguageFallback == "DMY" ? + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor7) : + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor6)); + add(FrenchDateTime.DefaultLanguageFallback == "DMY" ? + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor6) : + RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor7)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor8)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractor9)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.DateExtractorA)); + } + }; + + public static final List ImplicitDateList = new ArrayList() { + { + add(OnRegex); + add(RelaxedOnRegex); + add(SpecialDayRegex); + add(ThisRegex); + add(LastDateRegex); + add(NextDateRegex); + add(StrictWeekDay); + add(WeekDayOfMonthRegex); + add(SpecialDate); + } + }; + + public static final Pattern OfMonth = RegExpUtility.getSafeRegExp(FrenchDateTime.OfMonth); + public static final Pattern MonthEnd = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthEnd); + public static final Pattern WeekDayEnd = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekDayEnd); + public static final Pattern YearSuffix = RegExpUtility.getSafeRegExp(FrenchDateTime.YearSuffix); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.LessThanRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MoreThanRegex); + + public static final ImmutableMap DayOfWeek = FrenchDateTime.DayOfWeek; + public static final ImmutableMap MonthOfYear = FrenchDateTime.MonthOfYear; + + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final List implicitDateList; + + public FrenchDateExtractorConfiguration(final IOptionsConfiguration config) { + super(config.getOptions()); + integerExtractor = new IntegerExtractor(); + ordinalExtractor = new OrdinalExtractor(); + numberParser = new BaseNumberParser(new FrenchNumberParserConfiguration()); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration()); + utilityConfiguration = new FrenchDatetimeUtilityConfiguration(); + + implicitDateList = new ArrayList<>(ImplicitDateList); + } + + @Override + public Iterable getDateRegexList() { + return DateRegexList; + } + + @Override + public Iterable getImplicitDateList() { + return implicitDateList; + } + + @Override + public Pattern getOfMonth() { + return OfMonth; + } + + @Override + public Pattern getMonthEnd() { + return MonthEnd; + } + + @Override + public Pattern getWeekDayEnd() { + return WeekDayEnd; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getForTheRegex() { + return ForTheRegex; + } + + @Override + public Pattern getWeekDayAndDayOfMonthRegex() { + return WeekDayAndDayOfMonthRegex; + } + + @Override + public Pattern getRelativeMonthRegex() { + return RelativeMonthRegex; + } + + @Override + public Pattern getStrictRelativeRegex() { + return StrictRelativeRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return WeekDayRegex; + } + + @Override + public Pattern getPrefixArticleRegex() { + return PrefixArticleRegex; + } + + @Override + public Pattern getYearSuffix() { + return YearSuffix; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getRangeConnectorSymbolRegex() { + return RangeConnectorSymbolRegex; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public ImmutableMap getDayOfWeek() { + return DayOfWeek; + } + + @Override + public ImmutableMap getMonthOfYear() { + return MonthOfYear; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDatePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDatePeriodExtractorConfiguration.java new file mode 100644 index 000000000..0f87a53c9 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDatePeriodExtractorConfiguration.java @@ -0,0 +1,328 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.number.french.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.number.french.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.french.parsers.FrenchNumberParserConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FrenchDatePeriodExtractorConfiguration extends BaseOptionsConfiguration implements IDatePeriodExtractorConfiguration { + public static final Pattern TillRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TillRegex); + public static final Pattern RangeConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangeConnectorRegex); + public static final Pattern DayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DayRegex); + public static final Pattern MonthNumRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthNumRegex); + public static final Pattern IllegalYearRegex = RegExpUtility.getSafeRegExp(BaseDateTime.IllegalYearRegex); + public static final Pattern YearRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.YearRegex); + public static final Pattern RelativeMonthRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeMonthRegex); + public static final Pattern MonthRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthRegex); + public static final Pattern MonthSuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthSuffixRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DateUnitRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeUnitRegex); + public static final Pattern PastRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PastSuffixRegex); + public static final Pattern FutureRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NextSuffixRegex); + public static final Pattern FutureSuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.FutureSuffixRegex); + + // composite regexes + public static final Pattern SimpleCasesRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SimpleCasesRegex); + public static final Pattern MonthFrontSimpleCasesRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.MonthFrontSimpleCasesRegex); + public static final Pattern MonthFrontBetweenRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.MonthFrontBetweenRegex); + public static final Pattern BetweenRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.BetweenRegex); + public static final Pattern OneWordPeriodRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.OneWordPeriodRegex); + public static final Pattern MonthWithYearRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthWithYear); + public static final Pattern MonthNumWithYearRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.MonthNumWithYear); + public static final Pattern WeekOfMonthRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekOfMonthRegex); + public static final Pattern WeekOfYearRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekOfYearRegex); + public static final Pattern FollowedDateUnit = RegExpUtility.getSafeRegExp(FrenchDateTime.FollowedDateUnit); + public static final Pattern NumberCombinedWithDateUnit = RegExpUtility + .getSafeRegExp(FrenchDateTime.NumberCombinedWithDateUnit); + public static final Pattern QuarterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.QuarterRegex); + public static final Pattern QuarterRegexYearFront = RegExpUtility + .getSafeRegExp(FrenchDateTime.QuarterRegexYearFront); + public static final Pattern AllHalfYearRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AllHalfYearRegex); + public static final Pattern SeasonRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SeasonRegex); + public static final Pattern WhichWeekRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WhichWeekRegex); + public static final Pattern WeekOfRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekOfRegex); + public static final Pattern MonthOfRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MonthOfRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangeUnitRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.InConnectorRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.WithinNextPrefixRegex); + public static final Pattern LaterEarlyPeriodRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.LaterEarlyPeriodRegex); + public static final Pattern RestOfDateRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RestOfDateRegex); + public static final Pattern WeekWithWeekDayRangeRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.WeekWithWeekDayRangeRegex); + public static final Pattern YearPlusNumberRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.YearPlusNumberRegex); + public static final Pattern DecadeWithCenturyRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.DecadeWithCenturyRegex); + public static final Pattern YearPeriodRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.YearPeriodRegex); + public static final Pattern ComplexDatePeriodRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.ComplexDatePeriodRegex); + public static final Pattern RelativeDecadeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeDecadeRegex); + public static final Pattern ReferenceDatePeriodRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.ReferenceDatePeriodRegex); + public static final Pattern AgoRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AgoRegex); + public static final Pattern LaterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.LaterRegex); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.LessThanRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MoreThanRegex); + public static final Pattern CenturySuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.CenturySuffixRegex); + public static final Pattern NowRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NowRegex); + + public static final Iterable SimpleCasesRegexes = new ArrayList() { + { + add(SimpleCasesRegex); + add(BetweenRegex); + add(OneWordPeriodRegex); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.MonthWithYear)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.MonthNumWithYear)); + add(YearRegex); + add(YearPeriodRegex); + add(WeekOfYearRegex); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.WeekDayOfMonthRegex)); + add(MonthFrontBetweenRegex); + add(MonthFrontSimpleCasesRegex); + add(QuarterRegex); + add(QuarterRegexYearFront); + add(SeasonRegex); + add(LaterEarlyPeriodRegex); + add(YearPlusNumberRegex); + add(DecadeWithCenturyRegex); + add(RelativeDecadeRegex); + } + }; + + private static final Pattern fromRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.FromRegex); + private static final Pattern betweenRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.BetweenRegex); + + private final IDateTimeExtractor datePointExtractor; + private final IExtractor cardinalExtractor; + private final IExtractor ordinalExtractor; + private final IDateTimeExtractor durationExtractor; + private final IParser numberParser; + private final String[] durationDateRestrictions; + + public FrenchDatePeriodExtractorConfiguration(final IOptionsConfiguration config) { + super(config.getOptions()); + + datePointExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + cardinalExtractor = CardinalExtractor.getInstance(); + ordinalExtractor = new OrdinalExtractor(); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration()); + numberParser = new BaseNumberParser(new FrenchNumberParserConfiguration()); + + durationDateRestrictions = FrenchDateTime.DurationDateRestrictions.toArray(new String[0]); + } + + @Override + public Iterable getSimpleCasesRegexes() { + return SimpleCasesRegexes; + } + + @Override + public Pattern getIllegalYearRegex() { + return IllegalYearRegex; + } + + @Override + public Pattern getYearRegex() { + return YearRegex; + } + + @Override + public Pattern getTillRegex() { + return TillRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getFollowedDateUnit() { + return FollowedDateUnit; + } + + @Override + public Pattern getNumberCombinedWithDateUnit() { + return NumberCombinedWithDateUnit; + } + + @Override + public Pattern getPastRegex() { + return PastRegex; + } + + @Override + public Pattern getFutureRegex() { + return FutureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return FutureSuffixRegex; + } + + @Override + public Pattern getWeekOfRegex() { + return WeekOfRegex; + } + + @Override + public Pattern getMonthOfRegex() { + return MonthOfRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getYearPeriodRegex() { + return YearPeriodRegex; + } + + @Override + public Pattern getRelativeDecadeRegex() { + return RelativeDecadeRegex; + } + + @Override + public Pattern getReferenceDatePeriodRegex() { + return ReferenceDatePeriodRegex; + } + + @Override + public Pattern getAgoRegex() { + return AgoRegex; + } + + @Override + public Pattern getLaterRegex() { + return LaterRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public Pattern getCenturySuffixRegex() { + return CenturySuffixRegex; + } + + @Override + public Pattern getNowRegex() { + return NowRegex; + } + + @Override + public String[] getDurationDateRestrictions() { + return durationDateRestrictions; + } + + @Override + public ResultIndex getFromTokenIndex(final String text) { + int index = -1; + boolean result = false; + final Matcher matcher = fromRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(final String text) { + int index = -1; + boolean result = false; + final Matcher matcher = betweenRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(final String text) { + final Optional match = Arrays.stream(RegExpUtility.getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.ConnectorAndRegex), text)).findFirst(); + return match.isPresent() && match.get().length == text.trim().length(); + } + + @Override + public Pattern getComplexDatePeriodRegex() { + return ComplexDatePeriodRegex; + } + + @Override + public IDateTimeExtractor getDatePointExtractor() { + return datePointExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimeAltExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimeAltExtractorConfiguration.java new file mode 100644 index 000000000..a07c59c04 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimeAltExtractorConfiguration.java @@ -0,0 +1,86 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeAltExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class FrenchDateTimeAltExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimeAltExtractorConfiguration { + + public static final Pattern ThisPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ThisPrefixRegex); + public static final Pattern PreviousPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PreviousPrefixRegex); + public static final Pattern NextPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NextPrefixRegex); + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AmRegex); + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PmRegex); + public static final Pattern RangePrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangePrefixRegex); + public static final Iterable RelativePrefixList = new ArrayList() { + { + add(ThisPrefixRegex); + add(PreviousPrefixRegex); + add(NextPrefixRegex); + } + }; + public static final Iterable AmPmRegexList = new ArrayList() { + { + add(AmRegex); + add(PmRegex); + } + }; + private static final Pattern OrRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.OrRegex); + private static final Pattern DayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DayRegex); + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor datePeriodExtractor; + + public FrenchDateTimeAltExtractorConfiguration(final IOptionsConfiguration config) { + super(config.getOptions()); + dateExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + datePeriodExtractor = new BaseDatePeriodExtractor(new FrenchDatePeriodExtractorConfiguration(this)); + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + @Override + public Iterable getRelativePrefixList() { + return RelativePrefixList; + } + + @Override + public Iterable getAmPmRegexList() { + return AmPmRegexList; + } + + @Override + public Pattern getOrRegex() { + return OrRegex; + } + + @Override + public Pattern getThisPrefixRegex() { + return ThisPrefixRegex; + } + + @Override + public Pattern getDayRegex() { + return DayRegex; + } + + @Override + public Pattern getRangePrefixRegex() { + return RangePrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimeExtractorConfiguration.java new file mode 100644 index 000000000..da47f1fbc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimeExtractorConfiguration.java @@ -0,0 +1,171 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.utilities.FrenchDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; +import java.util.Arrays; +import java.util.regex.Pattern; + +public class FrenchDateTimeExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimeExtractorConfiguration { + + public static final Pattern PrepositionRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PrepositionRegex); + public static final Pattern NowRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NowRegex); + public static final Pattern SuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SuffixRegex); + + //TODO: modify it according to the corresponding English regex + + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeOfDayRegex); + public static final Pattern SpecificTimeOfDayRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.SpecificTimeOfDayRegex); + public static final Pattern TimeOfTodayAfterRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.TimeOfTodayAfterRegex); + public static final Pattern TimeOfTodayBeforeRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.TimeOfTodayBeforeRegex); + public static final Pattern SimpleTimeOfTodayAfterRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.SimpleTimeOfTodayAfterRegex); + public static final Pattern SimpleTimeOfTodayBeforeRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.SimpleTimeOfTodayBeforeRegex); + public static final Pattern SpecificEndOfRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SpecificEndOfRegex); + public static final Pattern UnspecificEndOfRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.UnspecificEndOfRegex); + + //TODO: add this for french + public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeUnitRegex); + public static final Pattern ConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ConnectorRegex); + public static final Pattern NumberAsTimeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NumberAsTimeRegex); + public static final Pattern DateNumberConnectorRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.DateNumberConnectorRegex); + public static final Pattern SuffixAfterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SuffixAfterRegex); + private final IExtractor integerExtractor; + private final IDateExtractor datePointExtractor; + private final IDateTimeExtractor timePointExtractor; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + public FrenchDateTimeExtractorConfiguration(final DateTimeOptions options) { + + super(options); + + integerExtractor = IntegerExtractor.getInstance(); + datePointExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + timePointExtractor = new BaseTimeExtractor(new FrenchTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration(options)); + + utilityConfiguration = new FrenchDatetimeUtilityConfiguration(); + } + + public FrenchDateTimeExtractorConfiguration() { + this(DateTimeOptions.None); + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IDateExtractor getDatePointExtractor() { + return datePointExtractor; + } + + @Override + public IDateTimeExtractor getTimePointExtractor() { + return timePointExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public Pattern getNowRegex() { + return NowRegex; + } + + @Override + public Pattern getSuffixRegex() { + return SuffixRegex; + } + + @Override + public Pattern getTimeOfTodayAfterRegex() { + return TimeOfTodayAfterRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayAfterRegex() { + return SimpleTimeOfTodayAfterRegex; + } + + @Override + public Pattern getTimeOfTodayBeforeRegex() { + return TimeOfTodayBeforeRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayBeforeRegex() { + return SimpleTimeOfTodayBeforeRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return TimeOfDayRegex; + } + + @Override + public Pattern getSpecificEndOfRegex() { + return SpecificEndOfRegex; + } + + @Override + public Pattern getUnspecificEndOfRegex() { + return UnspecificEndOfRegex; + } + + @Override + public Pattern getUnitRegex() { + return UnitRegex; + } + + @Override + public Pattern getNumberAsTimeRegex() { + return NumberAsTimeRegex; + } + + @Override + public Pattern getDateNumberConnectorRegex() { + return DateNumberConnectorRegex; + } + + @Override + public Pattern getSuffixAfterRegex() { + return SuffixAfterRegex; + } + + public boolean isConnector(String text) { + + text = text.trim(); + + final boolean isPreposition = Arrays.stream(RegExpUtility.getMatches(PrepositionRegex, text)).findFirst().isPresent(); + final boolean isConnector = Arrays.stream(RegExpUtility.getMatches(ConnectorRegex, text)).findFirst().isPresent(); + return (StringUtility.isNullOrEmpty(text) || isPreposition || isConnector); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..bbd3c23e1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDateTimePeriodExtractorConfiguration.java @@ -0,0 +1,283 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.number.french.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FrenchDateTimePeriodExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimePeriodExtractorConfiguration { + + public static final Pattern weekDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WeekDayRegex); + public static final Pattern NumberCombinedWithUnit = RegExpUtility + .getSafeRegExp(FrenchDateTime.TimeNumberCombinedWithUnit); + public static final Pattern RestOfDateTimeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RestOfDateTimeRegex); + public static final Pattern PeriodTimeOfDayWithDateRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.PeriodTimeOfDayWithDateRegex); + public static final Pattern RelativeTimeUnitRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.RelativeTimeUnitRegex); + public static final Pattern GeneralEndingRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.GeneralEndingRegex); + public static final Pattern MiddlePauseRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MiddlePauseRegex); + public static final Pattern AmDescRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AmDescRegex); + public static final Pattern PmDescRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PmDescRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.WithinNextPrefixRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DateUnitRegex); + public static final Pattern PrefixDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PrefixDayRegex); + public static final Pattern SuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SuffixRegex); + public static final Pattern BeforeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.BeforeRegex); + public static final Pattern AfterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AfterRegex); + public static final Pattern FromRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.FromRegex2); + public static final Pattern RangeConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangeConnectorRegex); + public static final Pattern BetweenRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.BetweenRegex); + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeOfDayRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeUnitRegex); + public static final Pattern TimeFollowedUnit = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeFollowedUnit); + public static final Iterable SimpleCases = new ArrayList() { + { + add(FrenchTimePeriodExtractorConfiguration.PureNumFromTo); + add(FrenchTimePeriodExtractorConfiguration.PureNumBetweenAnd); + add(FrenchDateTimeExtractorConfiguration.SpecificTimeOfDayRegex); + } + }; + private final String tokenBeforeDate; + private final IExtractor cardinalExtractor; + private final IDateTimeExtractor singleDateExtractor; + private final IDateTimeExtractor singleTimeExtractor; + private final IDateTimeExtractor singleDateTimeExtractor; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor timeZoneExtractor; + + public FrenchDateTimePeriodExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public FrenchDateTimePeriodExtractorConfiguration(final DateTimeOptions options) { + + super(options); + tokenBeforeDate = FrenchDateTime.TokenBeforeDate; + + cardinalExtractor = CardinalExtractor.getInstance(); + + singleDateExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + singleTimeExtractor = new BaseTimeExtractor(new FrenchTimeExtractorConfiguration(options)); + singleDateTimeExtractor = new BaseDateTimeExtractor(new FrenchDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration(options)); + timePeriodExtractor = new BaseTimePeriodExtractor(new FrenchTimePeriodExtractorConfiguration(options)); + timeZoneExtractor = new BaseTimeZoneExtractor(new FrenchTimeZoneExtractorConfiguration(options)); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IDateTimeExtractor getSingleDateExtractor() { + return singleDateExtractor; + } + + @Override + public IDateTimeExtractor getSingleTimeExtractor() { + return singleTimeExtractor; + } + + @Override + public IDateTimeExtractor getSingleDateTimeExtractor() { + return singleDateTimeExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + @Override + public Iterable getSimpleCasesRegex() { + return SimpleCases; + } + + @Override + public Pattern getPrepositionRegex() { + return FrenchDateTimeExtractorConfiguration.PrepositionRegex; + } + + @Override + public Pattern getTillRegex() { + return FrenchTimePeriodExtractorConfiguration.TillRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return FrenchDateTimeExtractorConfiguration.TimeOfDayRegex; + } + + @Override + public Pattern getFollowedUnit() { + return TimeFollowedUnit; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return FrenchDatePeriodExtractorConfiguration.PastRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return FrenchDatePeriodExtractorConfiguration.FutureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return FrenchDatePeriodExtractorConfiguration.FutureSuffixRegex; + } + + @Override + public Pattern getPrefixDayRegex() { + return PrefixDayRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return NumberCombinedWithUnit; + } + + @Override + public Pattern getWeekDayRegex() { + return weekDayRegex; + } + + @Override + public Pattern getPeriodTimeOfDayWithDateRegex() { + return PeriodTimeOfDayWithDateRegex; + } + + @Override + public Pattern getRelativeTimeUnitRegex() { + return RelativeTimeUnitRegex; + } + + @Override + public Pattern getRestOfDateTimeRegex() { + return RestOfDateTimeRegex; + } + + @Override + public Pattern getGeneralEndingRegex() { + return GeneralEndingRegex; + } + + @Override + public Pattern getMiddlePauseRegex() { + return MiddlePauseRegex; + } + + @Override + public Pattern getAmDescRegex() { + return AmDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return PmDescRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getSuffixRegex() { + return SuffixRegex; + } + + @Override + public Pattern getBeforeRegex() { + return BeforeRegex; + } + + @Override + public Pattern getAfterRegex() { + return AfterRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return FrenchDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + } + + @Override + public ResultIndex getFromTokenIndex(final String text) { + int index = -1; + boolean result = false; + final Matcher matcher = FromRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(final String text) { + int index = -1; + boolean result = false; + final Matcher matcher = BetweenRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(final String text) { + final Optional match = Arrays.stream(RegExpUtility.getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.ConnectorAndRegex), text)).findFirst(); + return match.isPresent() && match.get().length == text.trim().length(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDurationExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDurationExtractorConfiguration.java new file mode 100644 index 000000000..400d02329 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchDurationExtractorConfiguration.java @@ -0,0 +1,139 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.IDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.number.french.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.regex.Pattern; + +public class FrenchDurationExtractorConfiguration extends BaseOptionsConfiguration implements IDurationExtractorConfiguration { + + // TODO: Investigate if required + // public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.UnitRegex); + public static final Pattern SuffixAndRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SuffixAndRegex); + public static final Pattern FollowedUnit = RegExpUtility.getSafeRegExp(FrenchDateTime.DurationFollowedUnit); + public static final Pattern NumberCombinedWithUnit = RegExpUtility.getSafeRegExp(FrenchDateTime.NumberCombinedWithDurationUnit); + public static final Pattern AnUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AnUnitRegex); + public static final Pattern DuringRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DuringRegex); + public static final Pattern AllRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AllRegex); + public static final Pattern HalfRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.HalfRegex); + public static final Pattern ConjunctionRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ConjunctionRegex); + public static final Pattern InexactNumberRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.InexactNumberRegex); + public static final Pattern InexactNumberUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.InexactNumberUnitRegex); + public static final Pattern RelativeDurationUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeDurationUnitRegex); + public static final Pattern DurationUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DurationUnitRegex); + public static final Pattern DurationConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DurationConnectorRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MoreThanRegex); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.LessThanRegex); + + private final IExtractor cardinalExtractor; + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + + public FrenchDurationExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public FrenchDurationExtractorConfiguration(final DateTimeOptions options) { + + super(options); + + cardinalExtractor = CardinalExtractor.getInstance(); + unitMap = FrenchDateTime.UnitMap; + unitValueMap = FrenchDateTime.UnitValueMap; + } + + @Override + public Pattern getFollowedUnit() { + return FollowedUnit; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return NumberCombinedWithUnit; + } + + @Override + public Pattern getAnUnitRegex() { + return AnUnitRegex; + } + + @Override + public Pattern getDuringRegex() { + return DuringRegex; + } + + @Override + public Pattern getAllRegex() { + return AllRegex; + } + + @Override + public Pattern getHalfRegex() { + return HalfRegex; + } + + @Override + public Pattern getSuffixAndRegex() { + return SuffixAndRegex; + } + + @Override + public Pattern getConjunctionRegex() { + return ConjunctionRegex; + } + + @Override + public Pattern getInexactNumberRegex() { + return InexactNumberRegex; + } + + @Override + public Pattern getInexactNumberUnitRegex() { + return InexactNumberUnitRegex; + } + + @Override + public Pattern getRelativeDurationUnitRegex() { + return RelativeDurationUnitRegex; + } + + @Override + public Pattern getDurationUnitRegex() { + return DurationUnitRegex; + } + + @Override + public Pattern getDurationConnectorRegex() { + return DurationConnectorRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchHolidayExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchHolidayExtractorConfiguration.java new file mode 100644 index 000000000..fc3cef61e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchHolidayExtractorConfiguration.java @@ -0,0 +1,38 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.IHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class FrenchHolidayExtractorConfiguration extends BaseOptionsConfiguration implements IHolidayExtractorConfiguration { + + public static final Pattern H1 = RegExpUtility.getSafeRegExp(FrenchDateTime.HolidayRegex1); + + public static final Pattern H2 = RegExpUtility.getSafeRegExp(FrenchDateTime.HolidayRegex2); + + public static final Pattern H3 = RegExpUtility.getSafeRegExp(FrenchDateTime.HolidayRegex3); + + public static final Pattern H4 = RegExpUtility.getSafeRegExp(FrenchDateTime.HolidayRegex4); + + public static final Iterable HolidayRegexList = new ArrayList() { + { + add(H1); + add(H2); + add(H3); + add(H4); + } + }; + + public FrenchHolidayExtractorConfiguration() { + super(DateTimeOptions.None); + } + + @Override + public Iterable getHolidayRegexes() { + return HolidayRegexList; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchMergedExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchMergedExtractorConfiguration.java new file mode 100644 index 000000000..f49ec1307 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchMergedExtractorConfiguration.java @@ -0,0 +1,194 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeAltExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseHolidayExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseSetExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeListExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import com.microsoft.recognizers.text.number.french.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.javatuples.Pair; + +public class FrenchMergedExtractorConfiguration extends BaseOptionsConfiguration implements IMergedExtractorConfiguration { + + public static final Pattern BeforeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.BeforeRegex); + public static final Pattern AfterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AfterRegex); + public static final Pattern SinceRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SinceRegex); + public static final Pattern AroundRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AroundRegex); + public static final Pattern FromToRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.FromToRegex); + public static final Pattern SingleAmbiguousMonthRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.SingleAmbiguousMonthRegex); + public static final Pattern PrepositionSuffixRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.PrepositionSuffixRegex); + public static final Pattern AmbiguousRangeModifierPrefix = RegExpUtility + .getSafeRegExp(FrenchDateTime.AmbiguousRangeModifierPrefix); + public static final Pattern NumberEndingPattern = RegExpUtility.getSafeRegExp(FrenchDateTime.NumberEndingPattern); + public static final Pattern SuffixAfterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SuffixAfterRegex); + public static final Pattern UnspecificDatePeriodRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.UnspecificDatePeriodRegex); + public static final StringMatcher SuperfluousWordMatcher = new StringMatcher(); + public final Iterable> ambiguityFiltersDict = FrenchDateTime.AmbiguityFiltersDict.entrySet().stream().map(pair -> { + Pattern key = RegExpUtility.getSafeRegExp(pair.getKey()); + Pattern val = RegExpUtility.getSafeRegExp(pair.getValue()); + return new Pair(key, val); + }).collect(Collectors.toList()); + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor datePeriodExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor dateTimePeriodExtractor; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeExtractor setExtractor; + private final IDateTimeExtractor holidayExtractor; + private final IDateTimeZoneExtractor timeZoneExtractor; + private final IDateTimeListExtractor dateTimeAltExtractor; + private final IExtractor integerExtractor; + + public FrenchMergedExtractorConfiguration(final DateTimeOptions options) { + super(options); + + setExtractor = new BaseSetExtractor(new FrenchSetExtractorConfiguration(options)); + dateExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + timeExtractor = new BaseTimeExtractor(new FrenchTimeExtractorConfiguration(options)); + holidayExtractor = new BaseHolidayExtractor(new FrenchHolidayExtractorConfiguration()); + datePeriodExtractor = new BaseDatePeriodExtractor(new FrenchDatePeriodExtractorConfiguration(this)); + dateTimeExtractor = new BaseDateTimeExtractor(new FrenchDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration(options)); + timeZoneExtractor = new BaseTimeZoneExtractor(new FrenchTimeZoneExtractorConfiguration(options)); + dateTimeAltExtractor = new BaseDateTimeAltExtractor(new FrenchDateTimeAltExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new FrenchTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor( + new FrenchDateTimePeriodExtractorConfiguration(options)); + integerExtractor = new IntegerExtractor(); + } + + public final StringMatcher getSuperfluousWordMatcher() { + return SuperfluousWordMatcher; + } + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + public final IDateTimeExtractor getSetExtractor() { + return setExtractor; + } + + public final IDateTimeExtractor getHolidayExtractor() { + return holidayExtractor; + } + + public final IDateTimeZoneExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + public final IDateTimeListExtractor getDateTimeAltExtractor() { + return dateTimeAltExtractor; + } + + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + public final Iterable> getAmbiguityFiltersDict() { + return ambiguityFiltersDict; + } + + @Override + public Iterable getFilterWordRegexList() { + return null; + } + + public final Pattern getAfterRegex() { + return AfterRegex; + } + + public final Pattern getBeforeRegex() { + return BeforeRegex; + } + + public final Pattern getSinceRegex() { + return SinceRegex; + } + + public final Pattern getAroundRegex() { + return AroundRegex; + } + + public final Pattern getFromToRegex() { + return FromToRegex; + } + + public final Pattern getSingleAmbiguousMonthRegex() { + return SingleAmbiguousMonthRegex; + } + + public final Pattern getPrepositionSuffixRegex() { + return PrepositionSuffixRegex; + } + + public final Pattern getAmbiguousRangeModifierPrefix() { + return null; + } + + public final Pattern getPotentialAmbiguousRangeRegex() { + return null; + } + + public final Pattern getNumberEndingPattern() { + return NumberEndingPattern; + } + + public final Pattern getSuffixAfterRegex() { + return SuffixAfterRegex; + } + + public final Pattern getUnspecificDatePeriodRegex() { + return UnspecificDatePeriodRegex; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchSetExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchSetExtractorConfiguration.java new file mode 100644 index 000000000..64e4ce397 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchSetExtractorConfiguration.java @@ -0,0 +1,113 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ISetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.regex.Pattern; + +public class FrenchSetExtractorConfiguration extends BaseOptionsConfiguration implements ISetExtractorConfiguration { + + public static final Pattern PeriodicRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PeriodicRegex); + public static final Pattern EachUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.EachUnitRegex); + public static final Pattern EachPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.EachPrefixRegex); + public static final Pattern EachDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.EachDayRegex); + // TODO + public static final Pattern BeforeEachDayRegex = null; + public static final Pattern SetWeekDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SetWeekDayRegex); + public static final Pattern SetEachRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.SetEachRegex); + private final IDateTimeExtractor durationExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor datePeriodExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor dateTimePeriodExtractor; + + public FrenchSetExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public FrenchSetExtractorConfiguration(final DateTimeOptions options) { + super(options); + + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration()); + timeExtractor = new BaseTimeExtractor(new FrenchTimeExtractorConfiguration(options)); + dateExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + dateTimeExtractor = new BaseDateTimeExtractor(new FrenchDateTimeExtractorConfiguration(options)); + datePeriodExtractor = new BaseDatePeriodExtractor(new FrenchDatePeriodExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new FrenchTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor( + new FrenchDateTimePeriodExtractorConfiguration(options)); + } + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + public final IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + public final Pattern getLastRegex() { + return FrenchDateExtractorConfiguration.LastDateRegex; + } + + public final Pattern getEachPrefixRegex() { + return EachPrefixRegex; + } + + public final Pattern getPeriodicRegex() { + return PeriodicRegex; + } + + public final Pattern getEachUnitRegex() { + return EachUnitRegex; + } + + public final Pattern getEachDayRegex() { + return EachDayRegex; + } + + public final Pattern getBeforeEachDayRegex() { + return BeforeEachDayRegex; + } + + public final Pattern getSetWeekDayRegex() { + return SetWeekDayRegex; + } + + public final Pattern getSetEachRegex() { + return SetEachRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimeExtractorConfiguration.java new file mode 100644 index 000000000..cd823eb07 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimeExtractorConfiguration.java @@ -0,0 +1,87 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class FrenchTimeExtractorConfiguration extends BaseOptionsConfiguration implements ITimeExtractorConfiguration { + + public static final Pattern DescRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DescRegex); + public static final Pattern HourNumRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.HourNumRegex); + public static final Pattern MinuteNumRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.MinuteNumRegex); + + public static final Pattern OclockRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.OclockRegex); + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PmRegex); + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AmRegex); + + public static final Pattern LessThanOneHour = RegExpUtility.getSafeRegExp(FrenchDateTime.LessThanOneHour); + // public static final Pattern TensTimeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TensTimeRegex); + + public static final Pattern WrittenTimeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.WrittenTimeRegex); + public static final Pattern TimePrefix = RegExpUtility.getSafeRegExp(FrenchDateTime.TimePrefix); + public static final Pattern TimeSuffix = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeSuffix); + public static final Pattern BasicTime = RegExpUtility.getSafeRegExp(FrenchDateTime.BasicTime); + public static final Pattern IshRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.IshRegex); + + public static final Pattern AtRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AtRegex); + public static final Pattern ConnectNumRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ConnectNumRegex); + public static final Pattern TimeBeforeAfterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeBeforeAfterRegex); + public static final Iterable TimeRegexList = new ArrayList() { + { + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex1)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex2)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex3)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex4)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex5)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex6)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex7)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex8)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex9)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.TimeRegex10)); + add(ConnectNumRegex); + } + }; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeExtractor timeZoneExtractor; + + public FrenchTimeExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public FrenchTimeExtractorConfiguration(final DateTimeOptions options) { + super(options); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration()); + timeZoneExtractor = new BaseTimeZoneExtractor(new FrenchTimeZoneExtractorConfiguration(options)); + } + + public final Pattern getIshRegex() { + return IshRegex; + } + + public final Iterable getTimeRegexList() { + return TimeRegexList; + } + + public final Pattern getAtRegex() { + return AtRegex; + } + + public final Pattern getTimeBeforeAfterRegex() { + return TimeBeforeAfterRegex; + } + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + public final IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..b8f093757 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimePeriodExtractorConfiguration.java @@ -0,0 +1,150 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.french.utilities.FrenchDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.french.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FrenchTimePeriodExtractorConfiguration extends BaseOptionsConfiguration implements ITimePeriodExtractorConfiguration { + + public static final Pattern HourNumRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.HourNumRegex); + public static final Pattern PureNumFromTo = RegExpUtility.getSafeRegExp(FrenchDateTime.PureNumFromTo); + public static final Pattern PureNumBetweenAnd = RegExpUtility.getSafeRegExp(FrenchDateTime.PureNumBetweenAnd); + public static final Pattern SpecificTimeFromTo = RegExpUtility.getSafeRegExp(FrenchDateTime.SpecificTimeFromTo); + public static final Pattern SpecificTimeBetweenAnd = RegExpUtility + .getSafeRegExp(FrenchDateTime.SpecificTimeBetweenAnd); + // TODO: What are these? + // public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.UnitRegex); + // public static final Pattern FollowedUnit = RegExpUtility.getSafeRegExp(FrenchDateTime.FollowedUnit); + public static final Pattern NumberCombinedWithUnit = RegExpUtility + .getSafeRegExp(FrenchDateTime.TimeNumberCombinedWithUnit); + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeOfDayRegex); + public static final Pattern GeneralEndingRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.GeneralEndingRegex); + public static final Pattern TillRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TillRegex); + private static final Pattern FromRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.FromRegex2); + private static final Pattern RangeConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangeConnectorRegex); + private static final Pattern BetweenRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.BeforeRegex2); + public final IDateTimeExtractor timeZoneExtractor; + public final Iterable getSimpleCasesRegex = new ArrayList() { + { + add(PureNumFromTo); + add(PureNumBetweenAnd); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.PmRegex)); + add(RegExpUtility.getSafeRegExp(FrenchDateTime.AmRegex)); + } + }; + public final Iterable getPureNumberRegex = new ArrayList() { + { + add(PureNumFromTo); + add(PureNumBetweenAnd); + } + }; + private final String tokenBeforeDate; + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final IDateTimeExtractor singleTimeExtractor; + private final IExtractor integerExtractor; + + public FrenchTimePeriodExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public FrenchTimePeriodExtractorConfiguration(final DateTimeOptions options) { + + super(options); + + tokenBeforeDate = FrenchDateTime.TokenBeforeDate; + singleTimeExtractor = new BaseTimeExtractor(new FrenchTimeExtractorConfiguration(options)); + utilityConfiguration = new FrenchDatetimeUtilityConfiguration(); + integerExtractor = new IntegerExtractor(); + timeZoneExtractor = new BaseTimeZoneExtractor(new FrenchTimeZoneExtractorConfiguration(options)); + } + + public final String getTokenBeforeDate() { + return tokenBeforeDate; + } + + public final IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + public final IDateTimeExtractor getSingleTimeExtractor() { + return singleTimeExtractor; + } + + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + public IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + public Iterable getSimpleCasesRegex() { + return getSimpleCasesRegex; + } + + public Iterable getPureNumberRegex() { + return getPureNumberRegex; + } + + public final Pattern getTillRegex() { + return TillRegex; + } + + public final Pattern getTimeOfDayRegex() { + return TimeOfDayRegex; + } + + public final Pattern getGeneralEndingRegex() { + return GeneralEndingRegex; + } + + @Override + public ResultIndex getFromTokenIndex(final String text) { + int index = -1; + boolean result = false; + final Matcher matcher = FromRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(final String text) { + int index = -1; + boolean result = false; + final Matcher matcher = BetweenRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(final String text) { + final Optional match = Arrays + .stream(RegExpUtility.getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.ConnectorAndRegex), text)) + .findFirst(); + return match.isPresent() && match.get().length == text.trim().length(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimeZoneExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimeZoneExtractorConfiguration.java new file mode 100644 index 000000000..4ad339474 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/extractors/FrenchTimeZoneExtractorConfiguration.java @@ -0,0 +1,43 @@ +package com.microsoft.recognizers.text.datetime.french.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeZoneExtractorConfiguration; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class FrenchTimeZoneExtractorConfiguration extends BaseOptionsConfiguration implements ITimeZoneExtractorConfiguration { + public FrenchTimeZoneExtractorConfiguration(final DateTimeOptions options) { + super(options); + + } + + private Pattern directUtcRegex; + + public final Pattern getDirectUtcRegex() { + return directUtcRegex; + } + + private Pattern locationTimeSuffixRegex; + + public final Pattern getLocationTimeSuffixRegex() { + return locationTimeSuffixRegex; + } + + private StringMatcher locationMatcher; + + public final StringMatcher getLocationMatcher() { + return locationMatcher; + } + + private StringMatcher timeZoneMatcher; + + public final StringMatcher getTimeZoneMatcher() { + return timeZoneMatcher; + } + + public final ArrayList getAmbiguousTimezoneList() { + return new ArrayList<>(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchCommonDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchCommonDateTimeParserConfiguration.java new file mode 100644 index 000000000..580fa2346 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchCommonDateTimeParserConfiguration.java @@ -0,0 +1,292 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.utilities.FrenchDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDatePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimeAltParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDurationParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.BaseDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.french.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.number.french.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.number.french.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.french.parsers.FrenchNumberParserConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; + +public class FrenchCommonDateTimeParserConfiguration extends BaseDateParserConfiguration { + + private final IDateTimeUtilityConfiguration utilityConfiguration; + + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + private final ImmutableMap seasonMap; + private final ImmutableMap specialYearPrefixesMap; + private final ImmutableMap cardinalMap; + private final ImmutableMap dayOfWeek; + private final ImmutableMap monthOfYear; + private final ImmutableMap numbers; + private final ImmutableMap doubleNumbers; + private final ImmutableMap writtenDecades; + private final ImmutableMap specialDecadeCases; + + private final IExtractor cardinalExtractor; + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IParser numberParser; + + private final IDateTimeExtractor durationExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor datePeriodExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor dateTimePeriodExtractor; + + private final IDateTimeParser timeZoneParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser durationParser; + private final IDateTimeParser datePeriodParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser dateTimePeriodParser; + private final IDateTimeParser dateTimeAltParser; + + public FrenchCommonDateTimeParserConfiguration(final DateTimeOptions options) { + + super(options); + + utilityConfiguration = new FrenchDatetimeUtilityConfiguration(); + + unitMap = FrenchDateTime.UnitMap; + unitValueMap = FrenchDateTime.UnitValueMap; + seasonMap = FrenchDateTime.SeasonMap; + specialYearPrefixesMap = FrenchDateTime.SpecialYearPrefixesMap; + cardinalMap = FrenchDateTime.CardinalMap; + dayOfWeek = FrenchDateTime.DayOfWeek; + monthOfYear = FrenchDateTime.MonthOfYear; + numbers = FrenchDateTime.Numbers; + doubleNumbers = FrenchDateTime.DoubleNumbers; + writtenDecades = FrenchDateTime.WrittenDecades; + specialDecadeCases = FrenchDateTime.SpecialDecadeCases; + + cardinalExtractor = CardinalExtractor.getInstance(); + integerExtractor = new IntegerExtractor(); + ordinalExtractor = new OrdinalExtractor(); + + numberParser = new BaseNumberParser(new FrenchNumberParserConfiguration()); + + dateExtractor = new BaseDateExtractor(new FrenchDateExtractorConfiguration(this)); + timeExtractor = new BaseTimeExtractor(new FrenchTimeExtractorConfiguration(options)); + dateTimeExtractor = new BaseDateTimeExtractor(new FrenchDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration()); + datePeriodExtractor = new BaseDatePeriodExtractor(new FrenchDatePeriodExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new FrenchTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor( + new FrenchDateTimePeriodExtractorConfiguration(options)); + + timeZoneParser = new BaseTimeZoneParser(); + durationParser = new BaseDurationParser(new FrenchDurationParserConfiguration(this)); + dateParser = new BaseDateParser(new FrenchDateParserConfiguration(this)); + timeParser = new FrenchTimeParser(new FrenchTimeParserConfiguration(this)); + dateTimeParser = new BaseDateTimeParser(new FrenchDateTimeParserConfiguration(this)); + datePeriodParser = new BaseDatePeriodParser(new FrenchDatePeriodParserConfiguration(this)); + timePeriodParser = new BaseTimePeriodParser(new FrenchTimePeriodParserConfiguration(this)); + dateTimePeriodParser = new BaseDateTimePeriodParser(new FrenchDateTimePeriodParserConfiguration(this)); + dateTimeAltParser = new BaseDateTimeAltParser(new FrenchDateTimeAltParserConfiguration(this)); + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } + + @Override + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + @Override + public IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + @Override + public IDateTimeParser getDateTimeAltParser() { + return dateTimeAltParser; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } + + @Override + public ImmutableMap getSeasonMap() { + return seasonMap; + } + + @Override + public ImmutableMap getSpecialYearPrefixesMap() { + return specialYearPrefixesMap; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return ImmutableMap.builder() + .putAll(BaseDateTime.DayOfMonthDictionary) + .putAll(FrenchDateTime.DayOfMonth).build(); + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public ImmutableMap getDayOfWeek() { + return dayOfWeek; + } + + @Override + public ImmutableMap getDoubleNumbers() { + return doubleNumbers; + } + + @Override + public ImmutableMap getWrittenDecades() { + return writtenDecades; + } + + @Override + public ImmutableMap getSpecialDecadeCases() { + return specialDecadeCases; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateParserConfiguration.java new file mode 100644 index 000000000..0dfa50978 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateParserConfiguration.java @@ -0,0 +1,336 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.StringExtension; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FrenchDateParserConfiguration extends BaseOptionsConfiguration implements IDateParserConfiguration { + + private final String dateTokenPrefix; + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IExtractor cardinalExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeParser durationParser; + private final ImmutableMap unitMap; + private final Iterable dateRegexes; + private final Pattern onRegex; + private final Pattern specialDayRegex; + private final Pattern specialDayWithNumRegex; + private final Pattern nextRegex; + private final Pattern thisRegex; + private final Pattern lastRegex; + private final Pattern unitRegex; + private final Pattern weekDayRegex; + private final Pattern monthRegex; + private final Pattern weekDayOfMonthRegex; + private final Pattern forTheRegex; + private final Pattern weekDayAndDayOfMonthRegex; + private final Pattern relativeMonthRegex; + private final Pattern strictRelativeRegex; + private final Pattern yearSuffix; + private final Pattern relativeWeekDayRegex; + private final Pattern relativeDayRegex; + private final Pattern nextPrefixRegex; + private final Pattern previousPrefixRegex; + + private final ImmutableMap dayOfMonth; + private final ImmutableMap dayOfWeek; + private final ImmutableMap monthOfYear; + private final ImmutableMap cardinalMap; + private final List sameDayTerms; + private final List plusOneDayTerms; + private final List plusTwoDayTerms; + private final List minusOneDayTerms; + private final List minusTwoDayTerms; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + public FrenchDateParserConfiguration(final ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + dateTokenPrefix = FrenchDateTime.DateTokenPrefix; + integerExtractor = config.getIntegerExtractor(); + ordinalExtractor = config.getOrdinalExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = config.getDurationExtractor(); + dateExtractor = config.getDateExtractor(); + durationParser = config.getDurationParser(); + dateRegexes = Collections.unmodifiableList(FrenchDateExtractorConfiguration.DateRegexList); + onRegex = FrenchDateExtractorConfiguration.OnRegex; + specialDayRegex = FrenchDateExtractorConfiguration.SpecialDayRegex; + specialDayWithNumRegex = FrenchDateExtractorConfiguration.SpecialDayWithNumRegex; + nextRegex = FrenchDateExtractorConfiguration.NextDateRegex; + thisRegex = FrenchDateExtractorConfiguration.ThisRegex; + lastRegex = FrenchDateExtractorConfiguration.LastDateRegex; + unitRegex = FrenchDateExtractorConfiguration.DateUnitRegex; + weekDayRegex = FrenchDateExtractorConfiguration.WeekDayRegex; + monthRegex = FrenchDateExtractorConfiguration.MonthRegex; + weekDayOfMonthRegex = FrenchDateExtractorConfiguration.WeekDayOfMonthRegex; + forTheRegex = FrenchDateExtractorConfiguration.ForTheRegex; + weekDayAndDayOfMonthRegex = FrenchDateExtractorConfiguration.WeekDayAndDayOfMonthRegex; + relativeMonthRegex = FrenchDateExtractorConfiguration.RelativeMonthRegex; + strictRelativeRegex = FrenchDateExtractorConfiguration.StrictRelativeRegex; + yearSuffix = FrenchDateExtractorConfiguration.YearSuffix; + relativeWeekDayRegex = FrenchDateExtractorConfiguration.RelativeWeekDayRegex; + relativeDayRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeDayRegex); + nextPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NextPrefixRegex); + previousPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PreviousPrefixRegex); + dayOfMonth = config.getDayOfMonth(); + dayOfWeek = config.getDayOfWeek(); + monthOfYear = config.getMonthOfYear(); + cardinalMap = config.getCardinalMap(); + unitMap = config.getUnitMap(); + utilityConfiguration = config.getUtilityConfiguration(); + sameDayTerms = Collections.unmodifiableList(FrenchDateTime.SameDayTerms); + plusOneDayTerms = Collections.unmodifiableList(FrenchDateTime.PlusOneDayTerms); + plusTwoDayTerms = Collections.unmodifiableList(FrenchDateTime.PlusTwoDayTerms); + minusOneDayTerms = Collections.unmodifiableList(FrenchDateTime.MinusOneDayTerms); + minusTwoDayTerms = Collections.unmodifiableList(FrenchDateTime.MinusTwoDayTerms); + } + + @Override + public String getDateTokenPrefix() { + return dateTokenPrefix; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public Iterable getDateRegexes() { + return dateRegexes; + } + + @Override + public Pattern getOnRegex() { + return onRegex; + } + + @Override + public Pattern getSpecialDayRegex() { + return specialDayRegex; + } + + @Override + public Pattern getSpecialDayWithNumRegex() { + return specialDayWithNumRegex; + } + + @Override + public Pattern getNextRegex() { + return nextRegex; + } + + @Override + public Pattern getThisRegex() { + return thisRegex; + } + + @Override + public Pattern getLastRegex() { + return lastRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return weekDayRegex; + } + + @Override + public Pattern getMonthRegex() { + return monthRegex; + } + + @Override + public Pattern getWeekDayOfMonthRegex() { + return weekDayOfMonthRegex; + } + + @Override + public Pattern getForTheRegex() { + return forTheRegex; + } + + @Override + public Pattern getWeekDayAndDayOfMonthRegex() { + return weekDayAndDayOfMonthRegex; + } + + @Override + public Pattern getRelativeMonthRegex() { + return relativeMonthRegex; + } + + @Override + public Pattern getStrictRelativeRegex() { + return strictRelativeRegex; + } + + @Override + public Pattern getYearSuffix() { + return yearSuffix; + } + + @Override + public Pattern getRelativeWeekDayRegex() { + return relativeWeekDayRegex; + } + + @Override + public Pattern getRelativeDayRegex() { + return relativeDayRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return nextPrefixRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return previousPrefixRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } + + @Override + public ImmutableMap getDayOfWeek() { + return dayOfWeek; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public List getSameDayTerms() { + return sameDayTerms; + } + + @Override + public List getPlusOneDayTerms() { + return plusOneDayTerms; + } + + @Override + public List getMinusOneDayTerms() { + return minusOneDayTerms; + } + + @Override + public List getPlusTwoDayTerms() { + return plusTwoDayTerms; + } + + @Override + public List getMinusTwoDayTerms() { + return minusTwoDayTerms; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public Integer getSwiftMonthOrYear(final String text) { + final String trimmedText = text.trim().toLowerCase(Locale.ROOT); + int swift = 0; + + Matcher regexMatcher = nextPrefixRegex.matcher(trimmedText); + if (regexMatcher.find()) { + swift = 1; + } + + regexMatcher = previousPrefixRegex.matcher(trimmedText); + if (regexMatcher.find()) { + swift = -1; + } + + return swift; + } + + @Override + public Boolean isCardinalLast(final String text) { + final String trimmedText = text.trim().toLowerCase(); + + return trimmedText.endsWith("dernière") || trimmedText.endsWith("dernières") || trimmedText.endsWith( + "derniere") || trimmedText.endsWith("dernieres"); + } + + @Override + public String normalize(final String text) { + return StringExtension.normalize(text, ImmutableMap.of()); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDatePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDatePeriodParserConfiguration.java new file mode 100644 index 000000000..f3c1e4514 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDatePeriodParserConfiguration.java @@ -0,0 +1,559 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDatePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public class FrenchDatePeriodParserConfiguration extends BaseOptionsConfiguration implements IDatePeriodParserConfiguration { + + public static final Pattern nextPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NextPrefixRegex); + public static final Pattern previousPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PreviousPrefixRegex); + public static final Pattern thisPrefixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.ThisPrefixRegex); + public static final Pattern nextSuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.NextSuffixRegex); + public static final Pattern pastSuffixRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PastSuffixRegex); + public static final Pattern relativeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RelativeRegex); + public static final Pattern unspecificEndOfRangeRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.UnspecificEndOfRangeRegex); + private final String tokenBeforeDate; + + // Regex + private final IDateExtractor dateExtractor; + private final IExtractor cardinalExtractor; + private final IExtractor ordinalExtractor; + private final IDateTimeExtractor durationExtractor; + private final IExtractor integerExtractor; + private final IParser numberParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser durationParser; + private final Pattern monthFrontBetweenRegex; + private final Pattern betweenRegex; + private final Pattern monthFrontSimpleCasesRegex; + private final Pattern simpleCasesRegex; + private final Pattern oneWordPeriodRegex; + private final Pattern monthWithYear; + private final Pattern monthNumWithYear; + private final Pattern yearRegex; + private final Pattern pastRegex; + private final Pattern futureRegex; + private final Pattern futureSuffixRegex; + private final Pattern numberCombinedWithUnit; + private final Pattern weekOfMonthRegex; + private final Pattern weekOfYearRegex; + private final Pattern quarterRegex; + private final Pattern quarterRegexYearFront; + private final Pattern allHalfYearRegex; + private final Pattern seasonRegex; + private final Pattern whichWeekRegex; + private final Pattern weekOfRegex; + private final Pattern monthOfRegex; + private final Pattern inConnectorRegex; + private final Pattern withinNextPrefixRegex; + private final Pattern restOfDateRegex; + private final Pattern laterEarlyPeriodRegex; + private final Pattern weekWithWeekDayRangeRegex; + private final Pattern yearPlusNumberRegex; + private final Pattern decadeWithCenturyRegex; + private final Pattern yearPeriodRegex; + private final Pattern complexDatePeriodRegex; + private final Pattern relativeDecadeRegex; + private final Pattern referenceDatePeriodRegex; + private final Pattern agoRegex; + private final Pattern laterRegex; + private final Pattern lessThanRegex; + private final Pattern moreThanRegex; + private final Pattern centurySuffixRegex; + private final Pattern nowRegex; + // Dictionaries + private final ImmutableMap unitMap; + private final ImmutableMap cardinalMap; + private final ImmutableMap dayOfMonth; + private final ImmutableMap monthOfYear; + private final ImmutableMap seasonMap; + private final ImmutableMap specialYearPrefixesMap; + private final ImmutableMap writtenDecades; + private final ImmutableMap numbers; + private final ImmutableMap specialDecadeCases; + + public FrenchDatePeriodParserConfiguration(final ICommonDateTimeParserConfiguration config) { + super(config.getOptions()); + + tokenBeforeDate = FrenchDateTime.TokenBeforeDate; + cardinalExtractor = config.getCardinalExtractor(); + ordinalExtractor = config.getOrdinalExtractor(); + integerExtractor = config.getIntegerExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = config.getDurationExtractor(); + dateExtractor = config.getDateExtractor(); + durationParser = config.getDurationParser(); + dateParser = config.getDateParser(); + monthFrontBetweenRegex = FrenchDatePeriodExtractorConfiguration.MonthFrontBetweenRegex; + betweenRegex = FrenchDatePeriodExtractorConfiguration.BetweenRegex; + monthFrontSimpleCasesRegex = FrenchDatePeriodExtractorConfiguration.MonthFrontSimpleCasesRegex; + simpleCasesRegex = FrenchDatePeriodExtractorConfiguration.SimpleCasesRegex; + oneWordPeriodRegex = FrenchDatePeriodExtractorConfiguration.OneWordPeriodRegex; + monthWithYear = FrenchDatePeriodExtractorConfiguration.MonthWithYearRegex; + monthNumWithYear = FrenchDatePeriodExtractorConfiguration.MonthNumWithYearRegex; + yearRegex = FrenchDatePeriodExtractorConfiguration.YearRegex; + pastRegex = FrenchDatePeriodExtractorConfiguration.PastRegex; + futureRegex = FrenchDatePeriodExtractorConfiguration.FutureRegex; + futureSuffixRegex = FrenchDatePeriodExtractorConfiguration.FutureSuffixRegex; + numberCombinedWithUnit = FrenchDurationExtractorConfiguration.NumberCombinedWithUnit; + weekOfMonthRegex = FrenchDatePeriodExtractorConfiguration.WeekOfMonthRegex; + weekOfYearRegex = FrenchDatePeriodExtractorConfiguration.WeekOfYearRegex; + quarterRegex = FrenchDatePeriodExtractorConfiguration.QuarterRegex; + quarterRegexYearFront = FrenchDatePeriodExtractorConfiguration.QuarterRegexYearFront; + allHalfYearRegex = FrenchDatePeriodExtractorConfiguration.AllHalfYearRegex; + seasonRegex = FrenchDatePeriodExtractorConfiguration.SeasonRegex; + whichWeekRegex = FrenchDatePeriodExtractorConfiguration.WhichWeekRegex; + weekOfRegex = FrenchDatePeriodExtractorConfiguration.WeekOfRegex; + monthOfRegex = FrenchDatePeriodExtractorConfiguration.MonthOfRegex; + restOfDateRegex = FrenchDatePeriodExtractorConfiguration.RestOfDateRegex; + laterEarlyPeriodRegex = FrenchDatePeriodExtractorConfiguration.LaterEarlyPeriodRegex; + weekWithWeekDayRangeRegex = FrenchDatePeriodExtractorConfiguration.WeekWithWeekDayRangeRegex; + yearPlusNumberRegex = FrenchDatePeriodExtractorConfiguration.YearPlusNumberRegex; + decadeWithCenturyRegex = FrenchDatePeriodExtractorConfiguration.DecadeWithCenturyRegex; + yearPeriodRegex = FrenchDatePeriodExtractorConfiguration.YearPeriodRegex; + complexDatePeriodRegex = FrenchDatePeriodExtractorConfiguration.ComplexDatePeriodRegex; + relativeDecadeRegex = FrenchDatePeriodExtractorConfiguration.RelativeDecadeRegex; + inConnectorRegex = config.getUtilityConfiguration().getInConnectorRegex(); + withinNextPrefixRegex = FrenchDatePeriodExtractorConfiguration.WithinNextPrefixRegex; + referenceDatePeriodRegex = FrenchDatePeriodExtractorConfiguration.ReferenceDatePeriodRegex; + agoRegex = FrenchDatePeriodExtractorConfiguration.AgoRegex; + laterRegex = FrenchDatePeriodExtractorConfiguration.LaterRegex; + lessThanRegex = FrenchDatePeriodExtractorConfiguration.LessThanRegex; + moreThanRegex = FrenchDatePeriodExtractorConfiguration.MoreThanRegex; + centurySuffixRegex = FrenchDatePeriodExtractorConfiguration.CenturySuffixRegex; + nowRegex = FrenchDatePeriodExtractorConfiguration.NowRegex; + + unitMap = config.getUnitMap(); + cardinalMap = config.getCardinalMap(); + dayOfMonth = config.getDayOfMonth(); + monthOfYear = config.getMonthOfYear(); + seasonMap = config.getSeasonMap(); + specialYearPrefixesMap = config.getSpecialYearPrefixesMap(); + numbers = config.getNumbers(); + writtenDecades = config.getWrittenDecades(); + specialDecadeCases = config.getSpecialDecadeCases(); + } + + @Override + public final String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public final IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public final IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public final IParser getNumberParser() { + return numberParser; + } + + @Override + public final IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public final IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public final Pattern getMonthFrontBetweenRegex() { + return monthFrontBetweenRegex; + } + + @Override + public final Pattern getBetweenRegex() { + return betweenRegex; + } + + @Override + public final Pattern getMonthFrontSimpleCasesRegex() { + return monthFrontSimpleCasesRegex; + } + + @Override + public final Pattern getSimpleCasesRegex() { + return simpleCasesRegex; + } + + @Override + public final Pattern getOneWordPeriodRegex() { + return oneWordPeriodRegex; + } + + @Override + public final Pattern getMonthWithYear() { + return monthWithYear; + } + + @Override + public final Pattern getMonthNumWithYear() { + return monthNumWithYear; + } + + @Override + public final Pattern getYearRegex() { + return yearRegex; + } + + @Override + public final Pattern getPastRegex() { + return pastRegex; + } + + @Override + public final Pattern getFutureRegex() { + return futureRegex; + } + + @Override + public final Pattern getFutureSuffixRegex() { + return futureSuffixRegex; + } + + @Override + public final Pattern getNumberCombinedWithUnit() { + return numberCombinedWithUnit; + } + + @Override + public final Pattern getWeekOfMonthRegex() { + return weekOfMonthRegex; + } + + @Override + public final Pattern getWeekOfYearRegex() { + return weekOfYearRegex; + } + + @Override + public final Pattern getQuarterRegex() { + return quarterRegex; + } + + @Override + public final Pattern getQuarterRegexYearFront() { + return quarterRegexYearFront; + } + + @Override + public final Pattern getAllHalfYearRegex() { + return allHalfYearRegex; + } + + @Override + public final Pattern getSeasonRegex() { + return seasonRegex; + } + + @Override + public final Pattern getWhichWeekRegex() { + return whichWeekRegex; + } + + @Override + public final Pattern getWeekOfRegex() { + return weekOfRegex; + } + + @Override + public final Pattern getMonthOfRegex() { + return monthOfRegex; + } + + @Override + public final Pattern getInConnectorRegex() { + return inConnectorRegex; + } + + @Override + public final Pattern getWithinNextPrefixRegex() { + return withinNextPrefixRegex; + } + + @Override + public final Pattern getRestOfDateRegex() { + return restOfDateRegex; + } + + @Override + public final Pattern getLaterEarlyPeriodRegex() { + return laterEarlyPeriodRegex; + } + + @Override + public final Pattern getWeekWithWeekDayRangeRegex() { + return laterEarlyPeriodRegex; + } + + @Override + public final Pattern getYearPlusNumberRegex() { + return yearPlusNumberRegex; + } + + @Override + public final Pattern getDecadeWithCenturyRegex() { + return decadeWithCenturyRegex; + } + + @Override + public final Pattern getYearPeriodRegex() { + return yearPeriodRegex; + } + + @Override + public final Pattern getComplexDatePeriodRegex() { + return complexDatePeriodRegex; + } + + @Override + public final Pattern getRelativeDecadeRegex() { + return complexDatePeriodRegex; + } + + @Override + public final Pattern getReferenceDatePeriodRegex() { + return referenceDatePeriodRegex; + } + + @Override + public final Pattern getAgoRegex() { + return agoRegex; + } + + @Override + public final Pattern getLaterRegex() { + return laterRegex; + } + + @Override + public final Pattern getLessThanRegex() { + return lessThanRegex; + } + + @Override + public final Pattern getMoreThanRegex() { + return moreThanRegex; + } + + @Override + public final Pattern getCenturySuffixRegex() { + return centurySuffixRegex; + } + + @Override + public final Pattern getNextPrefixRegex() { + return nextPrefixRegex; + } + + @Override + public final Pattern getPastPrefixRegex() { + return previousPrefixRegex; + } + + @Override + public final Pattern getThisPrefixRegex() { + return thisPrefixRegex; + } + + @Override + public final Pattern getRelativeRegex() { + return relativeRegex; + } + + @Override + public final Pattern getUnspecificEndOfRangeRegex() { + return unspecificEndOfRangeRegex; + } + + @Override + public Pattern getNowRegex() { + return nowRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getSeasonMap() { + return seasonMap; + } + + @Override + public ImmutableMap getSpecialYearPrefixesMap() { + return specialYearPrefixesMap; + } + + @Override + public ImmutableMap getWrittenDecades() { + return writtenDecades; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public ImmutableMap getSpecialDecadeCases() { + return specialDecadeCases; + } + + @Override + public int getSwiftDayOrMonth(final String text) { + + final String trimmedText = text.trim().toLowerCase(); + int swift = 0; + + if (trimmedText.endsWith("prochain") || trimmedText.endsWith("prochaine")) { + swift = 1; + } + + if (trimmedText.endsWith("dernière") || + trimmedText.endsWith("dernières") || + trimmedText.endsWith("derniere") || + trimmedText.endsWith("dernieres") + ) { + swift = -1; + } + + return swift; + } + + @Override + public int getSwiftYear(final String text) { + + final String trimmedText = text.trim().toLowerCase(); + int swift = -10; + + if (trimmedText.endsWith("prochain") || trimmedText.endsWith("prochaine")) { + swift = 1; + } + + if (trimmedText.endsWith("dernière") || trimmedText.endsWith("dernières") || trimmedText + .endsWith("derniere") || trimmedText.endsWith("dernieres")) { + swift = -1; + } else if (trimmedText.startsWith("cette")) { + swift = 0; + } + + return swift; + } + + @Override + public boolean isFuture(final String text) { + final String trimmedText = text.trim().toLowerCase(); + + return FrenchDateTime.FutureStartTerms.stream().anyMatch(o -> trimmedText.startsWith(o)) || FrenchDateTime.FutureEndTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isLastCardinal(final String text) { + final String trimmedText = text.trim().toLowerCase(); + + final Optional matchLast = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)) + .findFirst(); + return matchLast.isPresent(); + } + + @Override + public boolean isMonthOnly(final String text) { + final String trimmedText = text.trim().toLowerCase(); + return FrenchDateTime.MonthTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isMonthToDate(final String text) { + final String trimmedText = text.trim().toLowerCase(); + return FrenchDateTime.MonthToDateTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isWeekend(final String text) { + final String trimmedText = text.trim().toLowerCase(); + return FrenchDateTime.WeekendTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isWeekOnly(final String text) { + final String trimmedText = text.trim().toLowerCase(); + + final boolean nextSuffix = Arrays.stream(RegExpUtility.getMatches(nextSuffixRegex, trimmedText)) + .findFirst().isPresent(); + final boolean pastSuffix = Arrays.stream(RegExpUtility.getMatches(pastSuffixRegex, trimmedText)) + .findFirst().isPresent(); + + return (FrenchDateTime.WeekTerms.stream().anyMatch(o -> trimmedText.endsWith(o)) || + (FrenchDateTime.WeekTerms.stream().anyMatch(o -> trimmedText.contains(o)) && (nextSuffix || pastSuffix))) && + !FrenchDateTime.WeekendTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isYearOnly(final String text) { + final String trimmedText = text.trim().toLowerCase(); + return FrenchDateTime.YearTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isYearToDate(final String text) { + final String trimmedText = text.trim().toLowerCase(); + + return FrenchDateTime.YearToDateTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimeAltParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimeAltParserConfiguration.java new file mode 100644 index 000000000..1911f9926 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimeAltParserConfiguration.java @@ -0,0 +1,48 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeAltParserConfiguration; + +public class FrenchDateTimeAltParserConfiguration implements IDateTimeAltParserConfiguration { + + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimePeriodParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser datePeriodParser; + + public FrenchDateTimeAltParserConfiguration(final ICommonDateTimeParserConfiguration config) { + dateTimeParser = config.getDateTimeParser(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + dateTimePeriodParser = config.getDateTimePeriodParser(); + timePeriodParser = config.getTimePeriodParser(); + datePeriodParser = config.getDatePeriodParser(); + } + + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + public IDateTimeParser getDateParser() { + return dateParser; + } + + public IDateTimeParser getTimeParser() { + return timeParser; + } + + public IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + public IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimeParserConfiguration.java new file mode 100644 index 000000000..911f0436e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimeParserConfiguration.java @@ -0,0 +1,258 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultTimex; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.regex.Pattern; + +public class FrenchDateTimeParserConfiguration extends BaseOptionsConfiguration implements IDateTimeParserConfiguration { + + public final String tokenBeforeDate; + public final String tokenBeforeTime; + + public final IDateTimeExtractor dateExtractor; + public final IDateTimeExtractor timeExtractor; + public final IDateTimeParser dateParser; + public final IDateTimeParser timeParser; + public final IExtractor cardinalExtractor; + public final IExtractor integerExtractor; + public final IParser numberParser; + public final IDateTimeExtractor durationExtractor; + public final IDateTimeParser durationParser; + + public final ImmutableMap unitMap; + public final ImmutableMap numbers; + + public final Pattern nowRegex; + public final Pattern amTimeRegex; + public final Pattern pmTimeRegex; + public final Pattern simpleTimeOfTodayAfterRegex; + public final Pattern simpleTimeOfTodayBeforeRegex; + public final Pattern specificTimeOfDayRegex; + public final Pattern specificEndOfRegex; + public final Pattern unspecificEndOfRegex; + public final Pattern unitRegex; + public final Pattern dateNumberConnectorRegex; + + public final IDateTimeUtilityConfiguration utilityConfiguration; + + public FrenchDateTimeParserConfiguration(final ICommonDateTimeParserConfiguration config) { + super(config.getOptions()); + + unitMap = config.getUnitMap(); + numbers = config.getNumbers(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + numberParser = config.getNumberParser(); + dateExtractor = config.getDateExtractor(); + timeExtractor = config.getTimeExtractor(); + durationParser = config.getDurationParser(); + integerExtractor = config.getIntegerExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + durationExtractor = config.getDurationExtractor(); + utilityConfiguration = config.getUtilityConfiguration(); + + tokenBeforeDate = FrenchDateTime.TokenBeforeDate; + tokenBeforeTime = FrenchDateTime.TokenBeforeTime; + + nowRegex = FrenchDateTimeExtractorConfiguration.NowRegex; + unitRegex = FrenchDateTimeExtractorConfiguration.UnitRegex; + specificEndOfRegex = FrenchDateTimeExtractorConfiguration.SpecificEndOfRegex; + unspecificEndOfRegex = FrenchDateTimeExtractorConfiguration.UnspecificEndOfRegex; + specificTimeOfDayRegex = FrenchDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + dateNumberConnectorRegex = FrenchDateTimeExtractorConfiguration.DateNumberConnectorRegex; + simpleTimeOfTodayAfterRegex = FrenchDateTimeExtractorConfiguration.SimpleTimeOfTodayAfterRegex; + simpleTimeOfTodayBeforeRegex = FrenchDateTimeExtractorConfiguration.SimpleTimeOfTodayBeforeRegex; + + pmTimeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PmRegex); + amTimeRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AMTimeRegex); + } + + @Override + public int getHour(final String text, final int hour) { + int result = hour; + + final String trimmedText = text.trim().toLowerCase(); + + if (trimmedText.endsWith("matin") && hour >= Constants.HalfDayHourCount) { + result -= Constants.HalfDayHourCount; + } else if (!trimmedText.endsWith("matin") && hour < Constants.HalfDayHourCount) { + result += Constants.HalfDayHourCount; + } + + return result; + } + + @Override + public ResultTimex getMatchedNowTimex(final String text) { + + final String trimmedText = text.trim().toLowerCase(); + + final String timex; + if (trimmedText.endsWith("maintenant")) { + timex = "PRESENT_REF"; + } else if (trimmedText.equals("récemment") || trimmedText.equals("précédemment") || trimmedText + .equals("auparavant")) { + timex = "PAST_REF"; + } else if (trimmedText.equals("dès que possible") || trimmedText.equals("dqp")) { + timex = "FUTURE_REF"; + } else { + timex = null; + return new ResultTimex(false, null); + } + + return new ResultTimex(true, timex); + } + + @Override + public int getSwiftDay(final String text) { + int swift = 0; + + final String trimmedText = text.trim().toLowerCase(); + + if (trimmedText.startsWith("prochain") || trimmedText.startsWith("prochain") || + trimmedText.startsWith("prochaine") || trimmedText.startsWith("prochaine")) { + swift = 1; + } else if (trimmedText.startsWith("dernier") || trimmedText.startsWith("dernière") || + trimmedText.startsWith("dernier") || trimmedText.startsWith("dernière")) { + swift = -1; + } + + return swift; + + } + + @Override + public boolean containsAmbiguousToken(final String text, final String matchedText) { + return false; + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public String getTokenBeforeTime() { + return tokenBeforeTime; + } + + @Override + public IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public Pattern getNowRegex() { + return nowRegex; + } + + public Pattern getAMTimeRegex() { + return amTimeRegex; + } + + public Pattern getPMTimeRegex() { + return pmTimeRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayAfterRegex() { + return simpleTimeOfTodayAfterRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayBeforeRegex() { + return simpleTimeOfTodayBeforeRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return specificTimeOfDayRegex; + } + + @Override + public Pattern getSpecificEndOfRegex() { + return specificEndOfRegex; + } + + @Override + public Pattern getUnspecificEndOfRegex() { + return unspecificEndOfRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getDateNumberConnectorRegex() { + return dateNumberConnectorRegex; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimePeriodParserConfiguration.java new file mode 100644 index 000000000..b4c67d729 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDateTimePeriodParserConfiguration.java @@ -0,0 +1,331 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.regex.Pattern; + +public class FrenchDateTimePeriodParserConfiguration extends BaseOptionsConfiguration implements IDateTimePeriodParserConfiguration { + + private final String tokenBeforeDate; + + private final IDateTimeExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor durationExtractor; + private final IExtractor cardinalExtractor; + + private final IParser numberParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser durationParser; + private final IDateTimeParser timeZoneParser; + + private final Pattern pureNumberFromToRegex; + private final Pattern pureNumberBetweenAndRegex; + private final Pattern specificTimeOfDayRegex; + private final Pattern timeOfDayRegex; + private final Pattern pastRegex; + private final Pattern futureRegex; + private final Pattern futureSuffixRegex; + private final Pattern numberCombinedWithUnitRegex; + private final Pattern unitRegex; + private final Pattern periodTimeOfDayWithDateRegex; + private final Pattern relativeTimeUnitRegex; + private final Pattern restOfDateTimeRegex; + private final Pattern amDescRegex; + private final Pattern pmDescRegex; + private final Pattern withinNextPrefixRegex; + private final Pattern prefixDayRegex; + private final Pattern beforeRegex; + private final Pattern afterRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap numbers; + + public FrenchDateTimePeriodParserConfiguration(final ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + tokenBeforeDate = FrenchDateTime.TokenBeforeDate; + + dateExtractor = config.getDateExtractor(); + timeExtractor = config.getTimeExtractor(); + dateTimeExtractor = config.getDateTimeExtractor(); + timePeriodExtractor = config.getTimePeriodExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + durationExtractor = config.getDurationExtractor(); + numberParser = config.getNumberParser(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + timePeriodParser = config.getTimePeriodParser(); + durationParser = config.getDurationParser(); + dateTimeParser = config.getDateTimeParser(); + timeZoneParser = config.getTimeZoneParser(); + + pureNumberFromToRegex = FrenchTimePeriodExtractorConfiguration.PureNumFromTo; + pureNumberBetweenAndRegex = FrenchTimePeriodExtractorConfiguration.PureNumBetweenAnd; + specificTimeOfDayRegex = FrenchDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + timeOfDayRegex = FrenchDateTimeExtractorConfiguration.TimeOfDayRegex; + pastRegex = FrenchDatePeriodExtractorConfiguration.PastRegex; + futureRegex = FrenchDatePeriodExtractorConfiguration.FutureRegex; + futureSuffixRegex = FrenchDatePeriodExtractorConfiguration.FutureSuffixRegex; + numberCombinedWithUnitRegex = FrenchDateTimePeriodExtractorConfiguration.NumberCombinedWithUnit; + unitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeUnitRegex); + periodTimeOfDayWithDateRegex = FrenchDateTimePeriodExtractorConfiguration.PeriodTimeOfDayWithDateRegex; + relativeTimeUnitRegex = FrenchDateTimePeriodExtractorConfiguration.RelativeTimeUnitRegex; + restOfDateTimeRegex = FrenchDateTimePeriodExtractorConfiguration.RestOfDateTimeRegex; + amDescRegex = FrenchDateTimePeriodExtractorConfiguration.AmDescRegex; + pmDescRegex = FrenchDateTimePeriodExtractorConfiguration.PmDescRegex; + withinNextPrefixRegex = FrenchDateTimePeriodExtractorConfiguration.WithinNextPrefixRegex; + prefixDayRegex = FrenchDateTimePeriodExtractorConfiguration.PrefixDayRegex; + beforeRegex = FrenchDateTimePeriodExtractorConfiguration.BeforeRegex; + afterRegex = FrenchDateTimePeriodExtractorConfiguration.AfterRegex; + + unitMap = config.getUnitMap(); + numbers = config.getNumbers(); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + @Override + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public Pattern getPureNumberFromToRegex() { + return pureNumberFromToRegex; + } + + @Override + public Pattern getPureNumberBetweenAndRegex() { + return pureNumberBetweenAndRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return specificTimeOfDayRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return timeOfDayRegex; + } + + @Override + public Pattern getPastRegex() { + return pastRegex; + } + + @Override + public Pattern getFutureRegex() { + return futureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return futureSuffixRegex; + } + + @Override + public Pattern getNumberCombinedWithUnitRegex() { + return numberCombinedWithUnitRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getPeriodTimeOfDayWithDateRegex() { + return periodTimeOfDayWithDateRegex; + } + + @Override + public Pattern getRelativeTimeUnitRegex() { + return relativeTimeUnitRegex; + } + + @Override + public Pattern getRestOfDateTimeRegex() { + return restOfDateTimeRegex; + } + + @Override + public Pattern getAmDescRegex() { + return amDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return pmDescRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return withinNextPrefixRegex; + } + + @Override + public Pattern getPrefixDayRegex() { + return prefixDayRegex; + } + + @Override + public Pattern getBeforeRegex() { + return beforeRegex; + } + + @Override + public Pattern getAfterRegex() { + return afterRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public MatchedTimeRangeResult getMatchedTimeRange(final String text, + String timeStr, + int beginHour, + int endHour, + int endMin) { + beginHour = 0; + endHour = 0; + endMin = 0; + + final String trimmedText = text.trim().toLowerCase(); + + if (RegExpUtility + .getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.MorningStartEndRegex), trimmedText).length > 0) { + timeStr = "TMO"; + beginHour = 8; + endHour = Constants.HalfDayHourCount; + } else if (RegExpUtility + .getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.AfternoonStartEndRegex), trimmedText).length + > 0) { + timeStr = "TAF"; + beginHour = Constants.HalfDayHourCount; + endHour = 16; + } else if (RegExpUtility + .getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.EveningStartEndRegex), trimmedText).length > 0) { + timeStr = "TEV"; + beginHour = 16; + endHour = 20; + } else if (RegExpUtility + .getMatches(RegExpUtility.getSafeRegExp(FrenchDateTime.NightStartEndRegex), trimmedText).length > 0) { + timeStr = "TNI"; + beginHour = 20; + endHour = 23; + endMin = 59; + } else { + return new MatchedTimeRangeResult(false, null, beginHour, endHour, endMin); + } + + return new MatchedTimeRangeResult(true, timeStr, beginHour, endHour, endMin); + } + + @Override + public int getSwiftPrefix(final String text) { + final String trimmedText = text.trim().toLowerCase(); + int swift = 0; + + if (trimmedText.startsWith("prochain") || trimmedText.endsWith("prochain") || + trimmedText.startsWith("prochaine") || trimmedText.endsWith("prochaine")) { + swift = 1; + } else if (trimmedText.startsWith("derniere") || trimmedText.startsWith("dernier") || + trimmedText.endsWith("derniere") || trimmedText.endsWith("dernier")) { + swift = -1; + } + + return swift; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDurationParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDurationParserConfiguration.java new file mode 100644 index 000000000..cf69ffcbc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchDurationParserConfiguration.java @@ -0,0 +1,144 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDurationParserConfiguration; +import java.util.regex.Pattern; + +public class FrenchDurationParserConfiguration extends BaseOptionsConfiguration implements IDurationParserConfiguration { + + private final IExtractor cardinalExtractor; + private final IExtractor durationExtractor; + private final IParser numberParser; + + private final Pattern numberCombinedWithUnit; + private final Pattern anUnitRegex; + private final Pattern duringRegex; + private final Pattern allDateUnitRegex; + private final Pattern halfDateUnitRegex; + private final Pattern suffixAndRegex; + private final Pattern followedUnit; + private final Pattern conjunctionRegex; + private final Pattern inexactNumberRegex; + private final Pattern inexactNumberUnitRegex; + private final Pattern durationUnitRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + private final ImmutableMap doubleNumbers; + + public FrenchDurationParserConfiguration(final ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + cardinalExtractor = config.getCardinalExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = new BaseDurationExtractor(new FrenchDurationExtractorConfiguration(), false); + numberCombinedWithUnit = FrenchDurationExtractorConfiguration.NumberCombinedWithUnit; + + anUnitRegex = FrenchDurationExtractorConfiguration.AnUnitRegex; + duringRegex = FrenchDurationExtractorConfiguration.DuringRegex; + allDateUnitRegex = FrenchDurationExtractorConfiguration.AllRegex; + halfDateUnitRegex = FrenchDurationExtractorConfiguration.HalfRegex; + suffixAndRegex = FrenchDurationExtractorConfiguration.SuffixAndRegex; + followedUnit = FrenchDurationExtractorConfiguration.FollowedUnit; + conjunctionRegex = FrenchDurationExtractorConfiguration.ConjunctionRegex; + inexactNumberRegex = FrenchDurationExtractorConfiguration.InexactNumberRegex; + inexactNumberUnitRegex = FrenchDurationExtractorConfiguration.InexactNumberUnitRegex; + durationUnitRegex = FrenchDurationExtractorConfiguration.DurationUnitRegex; + + unitMap = config.getUnitMap(); + unitValueMap = config.getUnitValueMap(); + doubleNumbers = config.getDoubleNumbers(); + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return numberCombinedWithUnit; + } + + @Override + public Pattern getAnUnitRegex() { + return anUnitRegex; + } + + @Override + public Pattern getDuringRegex() { + return duringRegex; + } + + @Override + public Pattern getAllDateUnitRegex() { + return allDateUnitRegex; + } + + @Override + public Pattern getHalfDateUnitRegex() { + return halfDateUnitRegex; + } + + @Override + public Pattern getSuffixAndRegex() { + return suffixAndRegex; + } + + @Override + public Pattern getFollowedUnit() { + return followedUnit; + } + + @Override + public Pattern getConjunctionRegex() { + return conjunctionRegex; + } + + @Override + public Pattern getInexactNumberRegex() { + return inexactNumberRegex; + } + + @Override + public Pattern getInexactNumberUnitRegex() { + return inexactNumberUnitRegex; + } + + @Override + public Pattern getDurationUnitRegex() { + return durationUnitRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } + + @Override + public ImmutableMap getDoubleNumbers() { + return doubleNumbers; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchHolidayParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchHolidayParserConfiguration.java new file mode 100644 index 000000000..d1cae9337 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchHolidayParserConfiguration.java @@ -0,0 +1,226 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseHolidayParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.IntFunction; + +public class FrenchHolidayParserConfiguration extends BaseHolidayParserConfiguration { + + public FrenchHolidayParserConfiguration() { + + setHolidayRegexList(FrenchHolidayExtractorConfiguration.HolidayRegexList); + + final HashMap> holidayNamesMap = new HashMap<>(); + for (final Map.Entry entry : FrenchDateTime.HolidayNames.entrySet()) { + if (entry.getValue() instanceof String[]) { + holidayNamesMap.put(entry.getKey(), Arrays.asList(entry.getValue())); + } + } + setHolidayNames(ImmutableMap.copyOf(holidayNamesMap)); + } + + private static LocalDateTime newYear(final int year) { + return DateUtil.safeCreateFromMinValue(year, 1, 1); + } + + private static LocalDateTime newYearEve(final int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 31); + } + + private static LocalDateTime christmasDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 25); + } + + private static LocalDateTime christmasEve(final int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 24); + } + + private static LocalDateTime valentinesDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 2, 14); + } + + private static LocalDateTime whiteLoverDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 14); + } + + private static LocalDateTime foolDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 4, 1); + } + + private static LocalDateTime girlsDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 7); + } + + private static LocalDateTime treePlantDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 12); + } + + private static LocalDateTime femaleDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 8); + } + + private static LocalDateTime childrenDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 1); + } + + private static LocalDateTime youthDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 4); + } + + private static LocalDateTime teacherDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 9, 10); + } + + private static LocalDateTime singlesDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 11); + } + + private static LocalDateTime maoBirthday(final int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 26); + } + + private static LocalDateTime inaugurationDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 1, 20); + } + + private static LocalDateTime groundhogDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 2, 2); + } + + private static LocalDateTime stPatrickDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 17); + } + + private static LocalDateTime stGeorgeDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 4, 23); + } + + private static LocalDateTime mayday(final int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 1); + } + + private static LocalDateTime cincoDeMayoday(final int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 5); + } + + private static LocalDateTime baptisteDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 24); + } + + private static LocalDateTime usaIndependenceDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 7, 4); + } + + private static LocalDateTime bastilleDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 7, 14); + } + + private static LocalDateTime halloweenDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 10, 31); + } + + private static LocalDateTime allHallowDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 1); + } + + private static LocalDateTime allSoulsday(final int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 2); + } + + private static LocalDateTime guyFawkesDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 5); + } + + private static LocalDateTime veteransday(final int year) { + return DateUtil.safeCreateFromMinValue(year, 11, 11); + } + + protected static LocalDateTime fathersDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 17); + } + + protected static LocalDateTime mothersDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 27); + } + + protected static LocalDateTime labourDay(final int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 1); + } + + @Override + public int getSwiftYear(final String text) { + final String trimmedText = text.trim(); + int swift = -10; + if (trimmedText.endsWith("prochain")) { + // next - 'l'annee prochain' + swift = 1; + } else if (trimmedText.endsWith("dernier")) { + // last - 'l'annee dernier' + swift = -1; + } else if (trimmedText.endsWith("cette")) { + // this - 'cette annees' + swift = 0; + } + + return swift; + } + + public String sanitizeHolidayToken(final String holiday) { + return holiday + .replaceAll(" ", "") + .replaceAll("'", ""); + } + + protected HashMap> initHolidayFuncs() { + return new HashMap>(super.initHolidayFuncs()) {{ + put("maosbirthday", FrenchHolidayParserConfiguration::maoBirthday); + put("yuandan", FrenchHolidayParserConfiguration::newYear); + put("teachersday", FrenchHolidayParserConfiguration::teacherDay); + put("singleday", FrenchHolidayParserConfiguration::singlesDay); + put("allsaintsday", FrenchHolidayParserConfiguration::halloweenDay); + put("youthday", FrenchHolidayParserConfiguration::youthDay); + put("childrenday", FrenchHolidayParserConfiguration::childrenDay); + put("femaleday", FrenchHolidayParserConfiguration::femaleDay); + put("treeplantingday", FrenchHolidayParserConfiguration::treePlantDay); + put("arborday", FrenchHolidayParserConfiguration::treePlantDay); + put("girlsday", FrenchHolidayParserConfiguration::girlsDay); + put("whiteloverday", FrenchHolidayParserConfiguration::whiteLoverDay); + put("loverday", FrenchHolidayParserConfiguration::valentinesDay); + put("christmas", FrenchHolidayParserConfiguration::christmasDay); + put("xmas", FrenchHolidayParserConfiguration::christmasDay); + put("newyear", FrenchHolidayParserConfiguration::newYear); + put("newyearday", FrenchHolidayParserConfiguration::newYear); + put("newyearsday", FrenchHolidayParserConfiguration::newYear); + put("inaugurationday", FrenchHolidayParserConfiguration::inaugurationDay); + put("groundhougday", FrenchHolidayParserConfiguration::groundhogDay); + put("valentinesday", FrenchHolidayParserConfiguration::valentinesDay); + put("stpatrickday", FrenchHolidayParserConfiguration::stPatrickDay); + put("aprilfools", FrenchHolidayParserConfiguration::foolDay); + put("stgeorgeday", FrenchHolidayParserConfiguration::stGeorgeDay); + put("mayday", FrenchHolidayParserConfiguration::mayday); + put("cincodemayoday", FrenchHolidayParserConfiguration::cincoDeMayoday); + put("baptisteday", FrenchHolidayParserConfiguration::baptisteDay); + put("usindependenceday", FrenchHolidayParserConfiguration::usaIndependenceDay); + put("independenceday", FrenchHolidayParserConfiguration::usaIndependenceDay); + put("bastilleday", FrenchHolidayParserConfiguration::bastilleDay); + put("halloweenday", FrenchHolidayParserConfiguration::halloweenDay); + put("allhallowday", FrenchHolidayParserConfiguration::allHallowDay); + put("allsoulsday", FrenchHolidayParserConfiguration::allSoulsday); + put("guyfawkesday", FrenchHolidayParserConfiguration::guyFawkesDay); + put("veteransday", FrenchHolidayParserConfiguration::veteransday); + put("christmaseve", FrenchHolidayParserConfiguration::christmasEve); + put("newyeareve", FrenchHolidayParserConfiguration::newYearEve); + put("fathersday", FrenchHolidayParserConfiguration::fathersDay); + put("mothersday", FrenchHolidayParserConfiguration::mothersDay); + put("labourday", FrenchHolidayParserConfiguration::labourDay); + } + }; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchMergedParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchMergedParserConfiguration.java new file mode 100644 index 000000000..afd508109 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchMergedParserConfiguration.java @@ -0,0 +1,75 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseHolidayParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseSetParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.IMergedParserConfiguration; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import java.util.regex.Pattern; + +public class FrenchMergedParserConfiguration extends FrenchCommonDateTimeParserConfiguration implements IMergedParserConfiguration { + + public FrenchMergedParserConfiguration(final DateTimeOptions options) { + super(options); + + beforeRegex = FrenchMergedExtractorConfiguration.BeforeRegex; + afterRegex = FrenchMergedExtractorConfiguration.AfterRegex; + sinceRegex = FrenchMergedExtractorConfiguration.SinceRegex; + aroundRegex = FrenchMergedExtractorConfiguration.AroundRegex; + suffixAfterRegex = FrenchMergedExtractorConfiguration.SuffixAfterRegex; + yearRegex = FrenchDatePeriodExtractorConfiguration.YearRegex; + superfluousWordMatcher = FrenchMergedExtractorConfiguration.SuperfluousWordMatcher; + + getParser = new BaseSetParser(new FrenchSetParserConfiguration(this)); + holidayParser = new BaseHolidayParser(new FrenchHolidayParserConfiguration()); + } + + private final Pattern beforeRegex; + private final Pattern afterRegex; + private final Pattern sinceRegex; + private final Pattern aroundRegex; + private final Pattern suffixAfterRegex; + private final Pattern yearRegex; + private final IDateTimeParser getParser; + private final IDateTimeParser holidayParser; + private final StringMatcher superfluousWordMatcher; + + public Pattern getBeforeRegex() { + return beforeRegex; + } + + public Pattern getAfterRegex() { + return afterRegex; + } + + public Pattern getSinceRegex() { + return sinceRegex; + } + + public Pattern getAroundRegex() { + return aroundRegex; + } + + public Pattern getSuffixAfterRegex() { + return suffixAfterRegex; + } + + public Pattern getYearRegex() { + return yearRegex; + } + + public IDateTimeParser getGetParser() { + return getParser; + } + + public IDateTimeParser getHolidayParser() { + return holidayParser; + } + + public StringMatcher getSuperfluousWordMatcher() { + return superfluousWordMatcher; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchSetParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchSetParserConfiguration.java new file mode 100644 index 000000000..b688497b0 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchSetParserConfiguration.java @@ -0,0 +1,195 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchSetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ISetParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.MatchedTimexResult; +import java.util.regex.Pattern; + +public class FrenchSetParserConfiguration extends BaseOptionsConfiguration implements ISetParserConfiguration { + + private final IDateTimeExtractor durationExtractor; + private final IDateTimeParser durationParser; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeParser timeParser; + private final IDateExtractor dateExtractor; + private final IDateTimeParser dateParser; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeParser dateTimeParser; + private final IDateTimeExtractor datePeriodExtractor; + private final IDateTimeParser datePeriodParser; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeParser timePeriodParser; + private final IDateTimeExtractor dateTimePeriodExtractor; + private final IDateTimeParser dateTimePeriodParser; + private final ImmutableMap unitMap; + private final Pattern eachPrefixRegex; + private final Pattern periodicRegex; + private final Pattern eachUnitRegex; + private final Pattern eachDayRegex; + private final Pattern setWeekDayRegex; + private final Pattern setEachRegex; + + public FrenchSetParserConfiguration(final ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + durationExtractor = config.getDurationExtractor(); + timeExtractor = config.getTimeExtractor(); + dateExtractor = config.getDateExtractor(); + dateTimeExtractor = config.getDateTimeExtractor(); + datePeriodExtractor = config.getDatePeriodExtractor(); + timePeriodExtractor = config.getTimePeriodExtractor(); + dateTimePeriodExtractor = config.getDateTimePeriodExtractor(); + + durationParser = config.getDurationParser(); + timeParser = config.getTimeParser(); + dateParser = config.getDateParser(); + dateTimeParser = config.getDateTimeParser(); + datePeriodParser = config.getDatePeriodParser(); + timePeriodParser = config.getTimePeriodParser(); + dateTimePeriodParser = config.getDateTimePeriodParser(); + unitMap = config.getUnitMap(); + + eachPrefixRegex = FrenchSetExtractorConfiguration.EachPrefixRegex; + periodicRegex = FrenchSetExtractorConfiguration.PeriodicRegex; + eachUnitRegex = FrenchSetExtractorConfiguration.EachUnitRegex; + eachDayRegex = FrenchSetExtractorConfiguration.EachDayRegex; + setWeekDayRegex = FrenchSetExtractorConfiguration.SetWeekDayRegex; + setEachRegex = FrenchSetExtractorConfiguration.SetEachRegex; + } + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + public final IDateTimeParser getDurationParser() { + return durationParser; + } + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + public final IDateTimeParser getTimeParser() { + return timeParser; + } + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + public final IDateTimeParser getDateParser() { + return dateParser; + } + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + public final IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + public final IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + public final IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + public final IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + public final ImmutableMap getUnitMap() { + return unitMap; + } + + public final Pattern getEachPrefixRegex() { + return eachPrefixRegex; + } + + public final Pattern getPeriodicRegex() { + return periodicRegex; + } + + public final Pattern getEachUnitRegex() { + return eachUnitRegex; + } + + public final Pattern getEachDayRegex() { + return eachDayRegex; + } + + public final Pattern getSetWeekDayRegex() { + return setWeekDayRegex; + } + + public final Pattern getSetEachRegex() { + return setEachRegex; + } + + public MatchedTimexResult getMatchedDailyTimex(final String text) { + final String trimmedText = text.trim(); + final String timex; + if (trimmedText.equals("quotidien") || trimmedText.equals("quotidienne") || + trimmedText.equals("jours") || trimmedText.equals("journellement")) { + // daily + timex = "P1D"; + } else if (trimmedText.equals("hebdomadaire")) { + // weekly + timex = "P1W"; + } else if (trimmedText.equals("bihebdomadaire")) { + // bi weekly + timex = "P2W"; + } else if (trimmedText.equals("mensuel") || trimmedText.equals("mensuelle")) { + // monthly + timex = "P1M"; + } else if (trimmedText.equals("annuel") || trimmedText.equals("annuellement")) { + // yearly/annually + timex = "P1Y"; + } else { + return new MatchedTimexResult(false, null); + } + + return new MatchedTimexResult(true, timex); + } + + public MatchedTimexResult getMatchedUnitTimex(final String text) { + final String trimmedText = text.trim(); + final String timex; + if (trimmedText.equals("jour") || trimmedText.equals("journee")) { + timex = "P1D"; + } else if (trimmedText.equals("semaine")) { + timex = "P1W"; + } else if (trimmedText.equals("mois")) { + timex = "P1M"; + } else if (trimmedText.equals("an") || trimmedText.equals("annee")) { + // year + timex = "P1Y"; + } else { + return new MatchedTimexResult(false, null); + } + + return new MatchedTimexResult(true, timex); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimeParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimeParser.java new file mode 100644 index 000000000..c66df2ae4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimeParser.java @@ -0,0 +1,57 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.StringUtility; +import java.time.LocalDateTime; + +public class FrenchTimeParser extends BaseTimeParser { + + public FrenchTimeParser(final ITimeParserConfiguration config) { + super(config); + } + + @Override + protected DateTimeResolutionResult internalParse(final String text, final LocalDateTime referenceTime) { + DateTimeResolutionResult innerResult = super.internalParse(text, referenceTime); + + if (!innerResult.getSuccess()) { + innerResult = parseIsh(text, referenceTime); + } + + return innerResult; + } + + // parse "noonish", "11-ish" + private DateTimeResolutionResult parseIsh(final String text, final LocalDateTime referenceTime) { + final DateTimeResolutionResult result = new DateTimeResolutionResult(); + + final ConditionalMatch match = RegexExtension.matchExact(FrenchTimeExtractorConfiguration.IshRegex, text, true); + if (match.getSuccess()) { + final String hourStr = match.getMatch().get().getGroup(Constants.HourGroupName).value; + int hour = Constants.HalfDayHourCount; + + if (!StringUtility.isNullOrEmpty(hourStr)) { + hour = Integer.parseInt(hourStr); + } + + result.setTimex(String.format("T%02d", hour)); + final LocalDateTime resultTime = DateUtil.safeCreateFromMinValue( + referenceTime.getYear(), + referenceTime.getMonthValue(), + referenceTime.getDayOfMonth(), + hour, 0, 0); + result.setFutureValue(resultTime); + result.setPastValue(resultTime); + result.setSuccess(true); + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimeParserConfiguration.java new file mode 100644 index 000000000..c33e542ba --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimeParserConfiguration.java @@ -0,0 +1,156 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.PrefixAdjustResult; +import com.microsoft.recognizers.text.datetime.parsers.config.SuffixAdjustResult; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; +import java.util.regex.Pattern; + +public class FrenchTimeParserConfiguration extends BaseOptionsConfiguration implements ITimeParserConfiguration { + + public final Pattern atRegex; + private final Iterable timeRegexes; + private final ImmutableMap numbers; + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final IDateTimeParser timeZoneParser; + public String timeTokenPrefix = FrenchDateTime.TimeTokenPrefix; + public Pattern mealTimeRegex; + + public FrenchTimeParserConfiguration(final ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + timeZoneParser = new BaseTimeZoneParser(); + + atRegex = FrenchTimeExtractorConfiguration.AtRegex; + timeRegexes = FrenchTimeExtractorConfiguration.TimeRegexList; + } + + @Override + public String getTimeTokenPrefix() { + return timeTokenPrefix; + } + + @Override + public Pattern getAtRegex() { + return atRegex; + } + + @Override + public Iterable getTimeRegexes() { + return timeRegexes; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public PrefixAdjustResult adjustByPrefix(final String prefix, int hour, int min, final boolean hasMin) { + + int deltaMin = 0; + final String trimmedPrefix = prefix.trim(); + + // c'este 8 heures et demie, - "it's half past 8" + if (trimmedPrefix.endsWith("demie")) { + deltaMin = 30; + } else if (trimmedPrefix.endsWith("un quart") || trimmedPrefix.endsWith("quart")) { + deltaMin = 15; + } else if (trimmedPrefix.endsWith("trois quarts")) { + deltaMin = 45; + } else { + final Match[] match = RegExpUtility + .getMatches(FrenchTimeExtractorConfiguration.LessThanOneHour, trimmedPrefix); + String minStr; + if (match.length > 0) { + minStr = match[0].getGroup("deltamin").value; + if (!StringUtility.isNullOrEmpty(minStr)) { + deltaMin = Integer.parseInt(minStr); + } else { + minStr = match[0].getGroup("deltaminnum").value; + deltaMin = numbers.get(minStr); + } + } + + } + + // 'to' i.e 'one to five' = 'un à cinq' + if (trimmedPrefix.endsWith("à")) { + deltaMin = -deltaMin; + } + + min += deltaMin; + if (min < 0) { + min += 60; + hour -= 1; + } + + return new PrefixAdjustResult(hour, min, true); + } + + @Override + public SuffixAdjustResult adjustBySuffix(final String suffix, + int hour, + final int min, + final boolean hasMin, + boolean hasAm, + boolean hasPm) { + + int deltaHour = 0; + final ConditionalMatch match = RegexExtension + .matchExact(FrenchTimeExtractorConfiguration.TimeSuffix, suffix, true); + + if (match.getSuccess()) { + final String oclockStr = match.getMatch().get().getGroup("heures").value; + if (StringUtility.isNullOrEmpty(oclockStr)) { + final String matchAmStr = match.getMatch().get().getGroup(Constants.AmGroupName).value; + if (!StringUtility.isNullOrEmpty(matchAmStr)) { + if (hour >= Constants.HalfDayHourCount) { + deltaHour = -Constants.HalfDayHourCount; + } + + hasAm = true; + } + + final String matchPmStr = match.getMatch().get().getGroup(Constants.PmGroupName).value; + if (!StringUtility.isNullOrEmpty(matchPmStr)) { + if (hour < Constants.HalfDayHourCount) { + deltaHour = Constants.HalfDayHourCount; + } + + hasPm = true; + } + } + } + + hour = (hour + deltaHour) % 24; + + return new SuffixAdjustResult(hour, min, hasMin, hasAm, hasPm); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimePeriodParserConfiguration.java new file mode 100644 index 000000000..acb8080a3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/parsers/FrenchTimePeriodParserConfiguration.java @@ -0,0 +1,158 @@ +package com.microsoft.recognizers.text.datetime.french.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.french.extractors.FrenchTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.TimeOfDayResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; +import java.util.regex.Pattern; + +public class FrenchTimePeriodParserConfiguration extends BaseOptionsConfiguration implements ITimePeriodParserConfiguration { + + private final IDateTimeExtractor timeExtractor; + private final IDateTimeParser timeParser; + private final IExtractor integerExtractor; + private final IDateTimeParser timeZoneParser; + + private final Pattern pureNumberFromToRegex; + private final Pattern pureNumberBetweenAndRegex; + private final Pattern specificTimeFromToRegex; + private final Pattern specificTimeBetweenAndRegex; + private final Pattern timeOfDayRegex; + private final Pattern generalEndingRegex; + private final Pattern tillRegex; + + private final ImmutableMap numbers; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + public FrenchTimePeriodParserConfiguration(final ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + timeExtractor = config.getTimeExtractor(); + integerExtractor = config.getIntegerExtractor(); + timeParser = config.getTimeParser(); + timeZoneParser = config.getTimeZoneParser(); + pureNumberFromToRegex = FrenchTimePeriodExtractorConfiguration.PureNumFromTo; + pureNumberBetweenAndRegex = FrenchTimePeriodExtractorConfiguration.PureNumBetweenAnd; + specificTimeFromToRegex = FrenchTimePeriodExtractorConfiguration.SpecificTimeFromTo; + specificTimeBetweenAndRegex = FrenchTimePeriodExtractorConfiguration.SpecificTimeBetweenAnd; + timeOfDayRegex = FrenchTimePeriodExtractorConfiguration.TimeOfDayRegex; + generalEndingRegex = FrenchTimePeriodExtractorConfiguration.GeneralEndingRegex; + tillRegex = FrenchTimePeriodExtractorConfiguration.TillRegex; + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public Pattern getPureNumberFromToRegex() { + return pureNumberFromToRegex; + } + + @Override + public Pattern getPureNumberBetweenAndRegex() { + return pureNumberBetweenAndRegex; + } + + @Override + public Pattern getSpecificTimeFromToRegex() { + return specificTimeFromToRegex; + } + + @Override + public Pattern getSpecificTimeBetweenAndRegex() { + return specificTimeBetweenAndRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return timeOfDayRegex; + } + + @Override + public Pattern getGeneralEndingRegex() { + return generalEndingRegex; + } + + @Override + public Pattern getTillRegex() { + return tillRegex; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public MatchedTimeRangeResult getMatchedTimexRange(final String text, + final String timex, + int beginHour, + int endHour, + int endMin) { + String mutatedText = text.trim(); + if (mutatedText.endsWith("s")) { + mutatedText = mutatedText.substring(0, mutatedText.length() - 1); + } + + final String trimmedText = mutatedText; + + beginHour = 0; + endHour = 0; + endMin = 0; + + String timeOfDay = ""; + if (FrenchDateTime.MorningTermList.stream().anyMatch(o -> trimmedText.endsWith(o))) { + timeOfDay = Constants.Morning; + } else if (FrenchDateTime.AfternoonTermList.stream().anyMatch(o -> trimmedText.endsWith(o))) { + timeOfDay = Constants.Afternoon; + } else if (FrenchDateTime.EveningTermList.stream().anyMatch(o -> trimmedText.endsWith(o))) { + timeOfDay = Constants.Evening; + } else if (FrenchDateTime.DaytimeTermList.stream().anyMatch(o -> trimmedText.equals(o))) { + timeOfDay = Constants.Daytime; + } else if (FrenchDateTime.NightTermList.stream().anyMatch(o -> trimmedText.endsWith(o))) { + timeOfDay = Constants.Night; + } else { + return new MatchedTimeRangeResult(false, null, beginHour, endHour, endMin); + } + + final TimeOfDayResolutionResult result = TimexUtility.parseTimeOfDay(timeOfDay); + + return new MatchedTimeRangeResult(true, result.getTimex(), result.getBeginHour(), result.getEndHour(), + result.getEndMin()); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/utilities/FrenchDatetimeUtilityConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/utilities/FrenchDatetimeUtilityConfiguration.java new file mode 100644 index 000000000..7dba9ecea --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/french/utilities/FrenchDatetimeUtilityConfiguration.java @@ -0,0 +1,87 @@ +package com.microsoft.recognizers.text.datetime.french.utilities; + +import com.microsoft.recognizers.text.datetime.resources.FrenchDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import java.util.regex.Pattern; + +public class FrenchDatetimeUtilityConfiguration implements IDateTimeUtilityConfiguration { + public static final Pattern AgoRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AgoRegex); + + public static final Pattern LaterRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.LaterRegex); + + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.InConnectorRegex); + + public static final Pattern WithinNextPrefixRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.WithinNextPrefixRegex); + + public static final Pattern AmDescRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AmDescRegex); + + public static final Pattern PmDescRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.PmDescRegex); + + public static final Pattern AmPmDescRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.AmPmDescRegex); + + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.RangeUnitRegex); + + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.TimeUnitRegex); + + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(FrenchDateTime.DateUnitRegex); + + public static final Pattern CommonDatePrefixRegex = RegExpUtility + .getSafeRegExp(FrenchDateTime.CommonDatePrefixRegex); + + @Override + public final Pattern getLaterRegex() { + return LaterRegex; + } + + @Override + public final Pattern getAgoRegex() { + return AgoRegex; + } + + @Override + public final Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public final Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public final Pattern getAmDescRegex() { + return AmDescRegex; + } + + @Override + public final Pattern getPmDescRegex() { + return PmDescRegex; + } + + @Override + public final Pattern getAmPmDescRegex() { + return AmPmDescRegex; + } + + @Override + public final Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public final Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public final Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public final Pattern getCommonDatePrefixRegex() { + return CommonDatePrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/models/DateTimeModel.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/models/DateTimeModel.java new file mode 100644 index 000000000..249271539 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/models/DateTimeModel.java @@ -0,0 +1,94 @@ +package com.microsoft.recognizers.text.datetime.models; + +import com.microsoft.recognizers.text.ExtendedModelResult; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IModel; +import com.microsoft.recognizers.text.ModelResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.DateTimeParseResult; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.utilities.FormatUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.stream.Collectors; + +public class DateTimeModel implements IModel { + + protected final IDateTimeExtractor extractor; + protected final IDateTimeParser parser; + + @Override + public String getModelTypeName() { + return Constants.MODEL_DATETIME; + } + + public DateTimeModel(IDateTimeParser parser, IDateTimeExtractor extractor) { + this.extractor = extractor; + this.parser = parser; + } + + @Override + public List parse(String query) { + return this.parse(query, LocalDateTime.now()); + } + + public List parse(String query, LocalDateTime reference) { + query = FormatUtility.preprocess(query); + + List parsedDateTimes = new ArrayList<>(); + + try { + List extractResults = extractor.extract(query, reference); + + for (ExtractResult result : extractResults) { + DateTimeParseResult parseResult = parser.parse(result, reference); + + if (parseResult.getValue() instanceof List) { + parsedDateTimes.addAll((List)parseResult.getValue()); + } else { + parsedDateTimes.add(parseResult); + } + } + + // Filter out ambiguous cases. Naïve approach. + parsedDateTimes = parser.filterResults(query, parsedDateTimes); + + } catch (Exception e) { + // Nothing to do. Exceptions in parse should not break users of recognizers. + // No result. + e.getMessage(); + } + + return parsedDateTimes.stream().map(this::getModelResult).collect(Collectors.toList()); + } + + private ModelResult getModelResult(DateTimeParseResult parsedDateTime) { + + int start = parsedDateTime.getStart(); + int end = parsedDateTime.getStart() + parsedDateTime.getLength() - 1; + String typeName = parsedDateTime.getType(); + SortedMap resolution = (SortedMap)parsedDateTime.getValue(); + String text = parsedDateTime.getText(); + + ModelResult result = new ModelResult(text, start, end, typeName, resolution); + + String[] types = parsedDateTime.getType().split("\\."); + String type = types[types.length - 1]; + if (type.equals(Constants.SYS_DATETIME_DATETIMEALT)) { + result = new ExtendedModelResult(result, getParentText(parsedDateTime)); + } + + return result; + } + + private String getParentText(DateTimeParseResult parsedDateTime) { + Map map = (Map)parsedDateTime.getData(); + Object result = map.get(ExtendedModelResult.ParentTextKey); + return String.valueOf(result); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateParser.java new file mode 100644 index 000000000..97a09e77b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateParser.java @@ -0,0 +1,739 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.AgoLaterUtil; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateContext; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class BaseDateParser implements IDateTimeParser { + + private final IDateParserConfiguration config; + + public BaseDateParser(IDateParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_DATE; + } + + @Override + public ParseResult parse(ExtractResult extResult) { + return parse(extResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + LocalDateTime referenceDate = reference; + + Object value = null; + + if (er.getType().equals(getParserName())) { + DateTimeResolutionResult innerResult = this.parseBasicRegexMatch(er.getText(), referenceDate); + + if (!innerResult.getSuccess()) { + innerResult = this.parseImplicitDate(er.getText(), referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseWeekdayOfMonth(er.getText(), referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseDurationWithAgoAndLater(er.getText(), referenceDate); + } + + // NumberWithMonth must be the second last one, because it only need to find a number and a month to get a "success" + if (!innerResult.getSuccess()) { + innerResult = this.parseNumberWithMonth(er.getText(), referenceDate); + } + + // SingleNumber last one + if (!innerResult.getSuccess()) { + innerResult = this.parseSingleNumber(er.getText(), referenceDate); + } + + if (innerResult.getSuccess()) { + ImmutableMap.Builder futureResolution = ImmutableMap.builder(); + futureResolution.put(TimeTypeConstants.DATE, DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getFutureValue())); + + innerResult.setFutureResolution(futureResolution.build()); + + ImmutableMap.Builder pastResolution = ImmutableMap.builder(); + pastResolution.put(TimeTypeConstants.DATE, DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getPastValue())); + + innerResult.setPastResolution(pastResolution.build()); + + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex()); + + return ret; + } + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } + + // parse basic patterns in DateRegexList + private DateTimeResolutionResult parseBasicRegexMatch(String text, LocalDateTime referenceDate) { + String trimmedText = text.trim(); + + for (Pattern regex : this.config.getDateRegexes()) { + int offset = 0; + String relativeStr = null; + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, trimmedText)).findFirst(); + + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(regex, this.config.getDateTokenPrefix() + trimmedText)).findFirst(); + // Handing cases like "(this)? 5.12" which only be recognized in "on (this)? 5.12" + if (match.isPresent()) { + offset = this.config.getDateTokenPrefix().length(); + relativeStr = match.get().getGroup("order").value.toLowerCase(); + } + } + + if (match.isPresent()) { + ConditionalMatch relativeRegex = RegexExtension.matchEnd(config.getStrictRelativeRegex(), text.substring(0, match.get().index), true); + boolean isContainRelative = relativeRegex.getSuccess() && match.get().index + match.get().length == trimmedText.length(); + if ((match.get().index == offset && match.get().length == trimmedText.length()) || isContainRelative) { + // Handing cases which contain relative term like "this 5/12" + if (match.get().index != offset) { + relativeStr = relativeRegex.getMatch().get().value; + } + + // LUIS value string will be set in Match2Date method + DateTimeResolutionResult ret = this.match2Date(match, referenceDate, relativeStr); + return ret; + } + } + } + + return new DateTimeResolutionResult(); + } + + // match several other cases + // including 'today', 'the day after tomorrow', 'on 13' + private DateTimeResolutionResult parseImplicitDate(String text, LocalDateTime referenceDate) { + String trimmedText = text.trim(); + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + // handle "on 12" + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getOnRegex(), this.config.getDateTokenPrefix() + trimmedText)).findFirst(); + if (match.isPresent() && match.get().index == 3 && match.get().length == trimmedText.length()) { + int month = referenceDate.getMonthValue(); + int year = referenceDate.getYear(); + String dayStr = match.get().getGroup("day").value.toLowerCase(); + int day = this.config.getDayOfMonth().get(dayStr); + + ret.setTimex(DateTimeFormatUtil.luisDate(-1, -1, day)); + + LocalDateTime futureDate; + LocalDateTime pastDate; + String tryStr = DateTimeFormatUtil.luisDate(year, month, day); + if (DateUtil.tryParse(tryStr) != null) { + futureDate = DateUtil.safeCreateFromMinValue(year, month, day); + pastDate = DateUtil.safeCreateFromMinValue(year, month, day); + + if (futureDate.isBefore(referenceDate)) { + futureDate = futureDate.plusMonths(1); + } + + if (pastDate.isEqual(referenceDate) || pastDate.isAfter(referenceDate)) { + pastDate = pastDate.minusMonths(1); + } + } else { + futureDate = DateUtil.safeCreateFromMinValue(year, month + 1, day); + pastDate = DateUtil.safeCreateFromMinValue(year, month - 1, day); + } + + ret.setFutureValue(futureDate); + ret.setPastValue(pastDate); + ret.setSuccess(true); + + return ret; + } + + // handle "today", "the day before yesterday" + ConditionalMatch exactMatch = RegexExtension.matchExact(this.config.getSpecialDayRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + int swift = getSwiftDay(exactMatch.getMatch().get().value); + + LocalDateTime value = referenceDate.toLocalDate().atStartOfDay().plusDays(swift); + + ret.setTimex(DateTimeFormatUtil.luisDate(value)); + ret.setFutureValue(value); + ret.setPastValue(value); + ret.setSuccess(true); + + return ret; + } + + // handle "two days from tomorrow" + exactMatch = RegexExtension.matchExact(this.config.getSpecialDayWithNumRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + + int swift = getSwiftDay(exactMatch.getMatch().get().getGroup("day").value); + List numErs = this.config.getIntegerExtractor().extract(trimmedText); + Object numberParsed = this.config.getNumberParser().parse(numErs.get(0)).getValue(); + int numOfDays = Math.round(((Double)numberParsed).floatValue()); + + LocalDateTime value = referenceDate.plusDays(numOfDays + swift); + + ret.setTimex(DateTimeFormatUtil.luisDate(value)); + ret.setFutureValue(value); + ret.setPastValue(value); + ret.setSuccess(true); + + return ret; + } + + // handle "two sundays from now" + exactMatch = RegexExtension.matchExact(this.config.getRelativeWeekDayRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + List numErs = this.config.getIntegerExtractor().extract(trimmedText); + Object numberParsed = this.config.getNumberParser().parse(numErs.get(0)).getValue(); + int num = Math.round(((Double)numberParsed).floatValue()); + + String weekdayStr = exactMatch.getMatch().get().getGroup("weekday").value.toLowerCase(); + LocalDateTime value = referenceDate; + + // Check whether the determined day of this week has passed. + if (value.getDayOfWeek().getValue() > this.config.getDayOfWeek().get(weekdayStr)) { + num--; + } + + while (num-- > 0) { + value = DateUtil.next(value, this.config.getDayOfWeek().get(weekdayStr)); + } + + ret.setTimex(DateTimeFormatUtil.luisDate(value)); + ret.setFutureValue(value); + ret.setPastValue(value); + ret.setSuccess(true); + + return ret; + } + + // handle "next Sunday" + exactMatch = RegexExtension.matchExact(this.config.getNextRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + String weekdayStr = exactMatch.getMatch().get().getGroup("weekday").value.toLowerCase(); + LocalDateTime value = DateUtil.next(referenceDate, this.config.getDayOfWeek().get(weekdayStr)); + + ret.setTimex(DateTimeFormatUtil.luisDate(value)); + ret.setFutureValue(value); + ret.setPastValue(value); + ret.setSuccess(true); + + return ret; + } + + // handle "this Friday" + exactMatch = RegexExtension.matchExact(this.config.getThisRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + String weekdayStr = exactMatch.getMatch().get().getGroup("weekday").value.toLowerCase(); + LocalDateTime value = DateUtil.thisDate(referenceDate, this.config.getDayOfWeek().get(weekdayStr)); + + ret.setTimex(DateTimeFormatUtil.luisDate(value)); + ret.setFutureValue(value); + ret.setPastValue(value); + ret.setSuccess(true); + + return ret; + } + + // handle "last Friday", "last mon" + exactMatch = RegexExtension.matchExact(this.config.getLastRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + String weekdayStr = exactMatch.getMatch().get().getGroup("weekday").value.toLowerCase(); + LocalDateTime value = DateUtil.last(referenceDate, this.config.getDayOfWeek().get(weekdayStr)); + + ret.setTimex(DateTimeFormatUtil.luisDate(value)); + ret.setFutureValue(value); + ret.setPastValue(value); + ret.setSuccess(true); + + return ret; + } + + // handle "Friday" + exactMatch = RegexExtension.matchExact(this.config.getWeekDayRegex(), trimmedText, true); + + if (exactMatch.getSuccess()) { + String weekdayStr = exactMatch.getMatch().get().getGroup("weekday").value.toLowerCase(); + int weekDay = this.config.getDayOfWeek().get(weekdayStr); + LocalDateTime value = DateUtil.thisDate(referenceDate, this.config.getDayOfWeek().get(weekdayStr)); + + if (weekDay == 0) { + weekDay = 7; + } + + if (weekDay < referenceDate.getDayOfWeek().getValue()) { + value = DateUtil.next(referenceDate, weekDay); + } + + ret.setTimex("XXXX-WXX-" + weekDay); + LocalDateTime futureDate = value; + LocalDateTime pastDate = value; + if (futureDate.isBefore(referenceDate)) { + futureDate = futureDate.plusDays(7); + } + + if (pastDate.isEqual(referenceDate) || pastDate.isAfter(referenceDate)) { + pastDate = pastDate.minusDays(7); + } + + ret.setFutureValue(futureDate); + ret.setPastValue(pastDate); + ret.setSuccess(true); + + return ret; + } + + // handle "for the 27th." + match = Arrays.stream(RegExpUtility.getMatches(this.config.getForTheRegex(), text)).findFirst(); + if (match.isPresent()) { + int day; + int month = referenceDate.getMonthValue(); + int year = referenceDate.getYear(); + String dayStr = match.get().getGroup("DayOfMonth").value.toLowerCase(); + + int start = match.get().getGroup("DayOfMonth").index; + int length = match.get().getGroup("DayOfMonth").length; + + // create a extract comments which content ordinal string of text + ExtractResult er = new ExtractResult(start, length, dayStr, null, null); + + Object numberParsed = this.config.getNumberParser().parse(er).getValue(); + day = Math.round(((Double)numberParsed).floatValue()); + + ret.setTimex(DateTimeFormatUtil.luisDate(-1, -1, day)); + + LocalDateTime futureDate; + String tryStr = DateTimeFormatUtil.luisDate(year, month, day); + + if (DateUtil.tryParse(tryStr) != null) { + futureDate = DateUtil.safeCreateFromMinValue(year, month, day); + } else { + futureDate = DateUtil.safeCreateFromMinValue(year, month + 1, day); + } + + ret.setFutureValue(futureDate); + ret.setPastValue(ret.getFutureValue()); + ret.setSuccess(true); + + return ret; + } + + // handling cases like 'Thursday the 21st', which both 'Thursday' and '21st' refer to a same date + match = Arrays.stream(RegExpUtility.getMatches(this.config.getWeekDayAndDayOfMonthRegex(), text)).findFirst(); + if (match.isPresent()) { + int month = referenceDate.getMonthValue(); + int year = referenceDate.getYear(); + String dayStr = match.get().getGroup("DayOfMonth").value.toLowerCase(); + + int start = match.get().getGroup("DayOfMonth").index; + int length = match.get().getGroup("DayOfMonth").length; + + // create a extract comments which content ordinal string of text + ExtractResult erTmp = new ExtractResult(start, length, dayStr, null, null); + + Object numberParsed = this.config.getNumberParser().parse(erTmp).getValue(); + int day = Math.round(((Double)numberParsed).floatValue()); + + // the validity of the phrase is guaranteed in the Date Extractor + ret.setTimex(DateTimeFormatUtil.luisDate(year, month, day)); + ret.setFutureValue(LocalDateTime.of(year, month, day, 0, 0)); + ret.setPastValue(LocalDateTime.of(year, month, day, 0, 0)); + ret.setSuccess(true); + + return ret; + } + + return ret; + } + + private DateTimeResolutionResult parseWeekdayOfMonth(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + String trimmedText = text.trim().toLowerCase(); + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getWeekDayOfMonthRegex(), this.config.getDateTokenPrefix() + trimmedText)).findFirst(); + if (!match.isPresent()) { + return ret; + } + + String cardinalStr = match.get().getGroup("cardinal").value; + String weekdayStr = match.get().getGroup("weekday").value; + String monthStr = match.get().getGroup("month").value; + Boolean noYear = false; + int year; + + int cardinal = this.config.isCardinalLast(cardinalStr) ? 5 : this.config.getCardinalMap().get(cardinalStr); + + int weekday = this.config.getDayOfWeek().get(weekdayStr); + int month; + if (StringUtility.isNullOrEmpty(monthStr)) { + int swift = this.config.getSwiftMonthOrYear(trimmedText); + + month = referenceDate.plusMonths(swift).getMonthValue(); + year = referenceDate.plusMonths(swift).getYear(); + } else { + month = this.config.getMonthOfYear().get(monthStr); + year = referenceDate.getYear(); + noYear = true; + } + + LocalDateTime value = computeDate(cardinal, weekday, month, year); + if (value.getMonthValue() != month) { + cardinal -= 1; + value = value.minusDays(7); + } + + LocalDateTime futureDate = value; + LocalDateTime pastDate = value; + if (noYear && futureDate.isBefore(referenceDate)) { + futureDate = computeDate(cardinal, weekday, month, year + 1); + if (futureDate.getMonthValue() != month) { + futureDate = futureDate.minusDays(7); + } + } + + if (noYear && (pastDate.isEqual(referenceDate) || pastDate.isAfter(referenceDate))) { + pastDate = computeDate(cardinal, weekday, month, year - 1); + if (pastDate.getMonthValue() != month) { + pastDate = pastDate.minusDays(7); + } + } + + // here is a very special case, timeX followe future date + ret.setTimex("XXXX-" + String.format("%02d", month) + "-WXX-" + weekday + "-#" + cardinal); + ret.setFutureValue(futureDate); + ret.setPastValue(pastDate); + ret.setSuccess(true); + + return ret; + } + + // Handle cases like "two days ago" + private DateTimeResolutionResult parseDurationWithAgoAndLater(String text, LocalDateTime referenceDate) { + + return AgoLaterUtil.parseDurationWithAgoAndLater(text, referenceDate, + config.getDurationExtractor(), config.getDurationParser(), config.getUnitMap(), config.getUnitRegex(), + config.getUtilityConfiguration(), this::getSwiftDay); + } + + // handle cases like "January first", "twenty-two of August" + // handle cases like "20th of next month" + private DateTimeResolutionResult parseNumberWithMonth(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + String trimmedText = text.trim().toLowerCase(); + int month = 0; + int day = 0; + int year = referenceDate.getYear(); + Boolean ambiguous = true; + + List er = this.config.getOrdinalExtractor().extract(trimmedText); + if (er.size() == 0) { + er = this.config.getIntegerExtractor().extract(trimmedText); + } + + if (er.size() == 0) { + return ret; + } + + Object numberParsed = this.config.getNumberParser().parse(er.get(0)).getValue(); + int num = Math.round(((Double)numberParsed).floatValue()); + + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getMonthRegex(), trimmedText)).findFirst(); + if (match.isPresent()) { + month = this.config.getMonthOfYear().get(match.get().value.trim()); + day = num; + + String suffix = trimmedText.substring((er.get(0).getStart() + er.get(0).getLength())); + + Optional matchYear = Arrays.stream(RegExpUtility.getMatches(this.config.getYearSuffix(), suffix)).findFirst(); + if (matchYear.isPresent()) { + year = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(matchYear.get()); + if (year != Constants.InvalidYear) { + ambiguous = false; + } + } + } + + // handling relatived month + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(this.config.getRelativeMonthRegex(), trimmedText)).findFirst(); + if (match.isPresent()) { + String monthStr = match.get().getGroup("order").value; + int swift = this.config.getSwiftMonthOrYear(monthStr); + month = referenceDate.plusMonths(swift).getMonthValue(); + year = referenceDate.plusMonths(swift).getYear(); + day = num; + ambiguous = false; + } + } + + // handling casesd like 'second Sunday' + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(this.config.getWeekDayRegex(), trimmedText)).findFirst(); + if (match.isPresent()) { + month = referenceDate.getMonthValue(); + // resolve the date of wanted week day + int wantedWeekDay = this.config.getDayOfWeek().get(match.get().getGroup("weekday").value); + LocalDateTime firstDate = DateUtil.safeCreateFromMinValue(referenceDate.getYear(), referenceDate.getMonthValue(), 1); + int firstWeekDay = firstDate.getDayOfWeek().getValue(); + LocalDateTime firstWantedWeekDay = firstDate.plusDays(wantedWeekDay > firstWeekDay ? wantedWeekDay - firstWeekDay : wantedWeekDay - firstWeekDay + 7); + int answerDay = firstWantedWeekDay.getDayOfMonth() + (num - 1) * 7; + day = answerDay; + ambiguous = false; + } + } + + if (!match.isPresent()) { + return ret; + } + + // for LUIS format value string + LocalDateTime futureDate = DateUtil.safeCreateFromMinValue(year, month, day); + LocalDateTime pastDate = DateUtil.safeCreateFromMinValue(year, month, day); + + if (ambiguous) { + ret.setTimex(DateTimeFormatUtil.luisDate(-1, month, day)); + if (futureDate.isBefore(referenceDate)) { + futureDate = futureDate.plusYears(1); + } + + if (pastDate.isEqual(referenceDate) || pastDate.isAfter(referenceDate)) { + pastDate = pastDate.minusYears(1); + } + } else { + ret.setTimex(DateTimeFormatUtil.luisDate(year, month, day)); + } + + ret.setFutureValue(futureDate); + ret.setPastValue(pastDate); + ret.setSuccess(true); + + return ret; + } + + // handle cases like "the 27th". In the extractor, only the unmatched weekday and date will output this date. + private DateTimeResolutionResult parseSingleNumber(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + String trimmedText = text.trim().toLowerCase(); + int day = 0; + + List er = this.config.getOrdinalExtractor().extract(trimmedText); + if (er.size() == 0) { + er = this.config.getIntegerExtractor().extract(trimmedText); + } + + if (er.size() == 0) { + return ret; + } + + Object numberParsed = this.config.getNumberParser().parse(er.get(0)).getValue(); + day = Math.round(((Double)numberParsed).floatValue()); + + int month = referenceDate.getMonthValue(); + int year = referenceDate.getYear(); + + // for LUIS format value string + ret.setTimex(DateTimeFormatUtil.luisDate(-1, -1, day)); + LocalDateTime pastDate = DateUtil.safeCreateFromMinValue(year, month, day); + LocalDateTime futureDate = DateUtil.safeCreateFromMinValue(year, month, day); + + if (!futureDate.isEqual(LocalDateTime.MIN) && futureDate.isBefore(referenceDate)) { + futureDate = futureDate.plusMonths(1); + } + + if (!pastDate.isEqual(LocalDateTime.MIN) && (pastDate.isEqual(referenceDate) || pastDate.isAfter(referenceDate))) { + pastDate = pastDate.minusMonths(1); + } + + ret.setFutureValue(futureDate); + ret.setPastValue(pastDate); + ret.setSuccess(true); + + return ret; + } + + private static LocalDateTime computeDate(int cardinal, int weekday, int month, int year) { + LocalDateTime firstDay = DateUtil.safeCreateFromMinValue(year, month, 1); + LocalDateTime firstWeekday = DateUtil.thisDate(firstDay, weekday); + int dayOfWeekOfFirstDay = firstDay.getDayOfWeek().getValue(); + + if (weekday == 0) { + weekday = 7; + } + + if (dayOfWeekOfFirstDay == 0) { + dayOfWeekOfFirstDay = 7; + } + + if (weekday < dayOfWeekOfFirstDay) { + firstWeekday = DateUtil.next(firstDay, weekday); + } + + return firstWeekday.plusDays(7 * (cardinal - 1)); + } + + // parse a regex match which includes 'day', 'month' and 'year' (optional) group + private DateTimeResolutionResult match2Date(Optional match, LocalDateTime referenceDate, String relativeStr) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + int month = 0; + int day = 0; + int year = 0; + + String monthStr = match.get().getGroup("month").value.toLowerCase(); + String dayStr = match.get().getGroup("day").value.toLowerCase(); + String weekdayStr = match.get().getGroup("weekday").value.toLowerCase(); + String yearStr = match.get().getGroup("year").value.toLowerCase(); + String writtenYearStr = match.get().getGroup("fullyear").value.toLowerCase(); + + if (this.config.getMonthOfYear().containsKey(monthStr) && this.config.getDayOfMonth().containsKey(dayStr)) { + + month = this.config.getMonthOfYear().get(monthStr); + day = this.config.getDayOfMonth().get(dayStr); + + if (!StringUtility.isNullOrEmpty(yearStr)) { + + year = this.config.getDateExtractor().getYearFromText(match.get()); + + } else if (!StringUtility.isNullOrEmpty(yearStr)) { + + year = Integer.parseInt(yearStr); + if (year < 100 && year >= Constants.MinTwoDigitYearPastNum) { + year += 1900; + } else if (year >= 0 && year < Constants.MaxTwoDigitYearFutureNum) { + year += 2000; + } + } + } + + Boolean noYear = false; + if (year == 0) { + year = referenceDate.getYear(); + if (!StringUtility.isNullOrEmpty(relativeStr)) { + int swift = config.getSwiftMonthOrYear(relativeStr); + + // @TODO Improve handling of next/last in particular cases "next friday 5/12" when the next friday is not 5/12. + if (!StringUtility.isNullOrEmpty(weekdayStr)) { + swift = 0; + } + year += swift; + } else { + noYear = true; + } + + ret.setTimex(DateTimeFormatUtil.luisDate(-1, month, day)); + } else { + ret.setTimex(DateTimeFormatUtil.luisDate(year, month, day)); + } + + HashMap futurePastDates = DateContext.generateDates(noYear, referenceDate, year, month, day); + LocalDateTime futureDate = futurePastDates.get(Constants.FutureDate); + LocalDateTime pastDate = futurePastDates.get(Constants.PastDate); + + ret.setFutureValue(futureDate); + ret.setPastValue(pastDate); + ret.setSuccess(true); + + return ret; + } + + private int getSwiftDay(String text) { + String trimmedText = this.config.normalize(text.trim().toLowerCase()); + int swift = 0; + + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getRelativeDayRegex(), text)).findFirst(); + + // The sequence here is important + // As suffix "day before yesterday" should be matched before suffix "day before" or "yesterday" + if (config.getSameDayTerms().contains(trimmedText)) { + swift = 0; + } else if (endsWithTerms(trimmedText, config.getPlusTwoDayTerms())) { + swift = 2; + } else if (endsWithTerms(trimmedText, config.getMinusTwoDayTerms())) { + swift = -2; + } else if (endsWithTerms(trimmedText, config.getPlusOneDayTerms())) { + swift = 1; + } else if (endsWithTerms(trimmedText, config.getMinusOneDayTerms())) { + swift = -1; + } else if (match.isPresent()) { + swift = getSwift(text); + } + + return swift; + } + + private int getSwift(String text) { + String trimmedText = text.trim().toLowerCase(); + + int swift = 0; + if (RegExpUtility.getMatches(this.config.getNextPrefixRegex(), trimmedText).length > 0) { + swift = 1; + } else if (RegExpUtility.getMatches(this.config.getPastPrefixRegex(), trimmedText).length > 0) { + swift = -1; + } + + return swift; + } + + private boolean endsWithTerms(String text, List terms) { + boolean result = false; + + for (String term : terms) { + if (text.endsWith(term)) { + result = true; + break; + } + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDatePeriodParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDatePeriodParser.java new file mode 100644 index 000000000..f3d5c94a3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDatePeriodParser.java @@ -0,0 +1,1897 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DatePeriodTimexType; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.parsers.config.IDatePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateContext; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.DurationParsingUtil; +import com.microsoft.recognizers.text.datetime.utilities.GetModAndDateResult; +import com.microsoft.recognizers.text.datetime.utilities.NthBusinessDayResult; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; +import com.microsoft.recognizers.text.utilities.IntegerUtility; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.sql.Time; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.javatuples.Pair; + +public class BaseDatePeriodParser implements IDateTimeParser { + + private static final String parserName = Constants.SYS_DATETIME_DATEPERIOD; //"DatePeriod"; + private static boolean inclusiveEndPeriod = false; + + private final IDatePeriodParserConfiguration config; + + public BaseDatePeriodParser(IDatePeriodParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return parserName; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime refDate) { + + DateTimeResolutionResult value = null; + if (er.getType().equals(parserName)) { + DateTimeResolutionResult innerResult = parseBaseDatePeriod(er.getText(), refDate); + + if (!innerResult.getSuccess()) { + innerResult = parseComplexDatePeriod(er.getText(), refDate); + } + + if (innerResult.getSuccess()) { + if (innerResult.getMod() != null && innerResult.getMod().equals(Constants.BEFORE_MOD)) { + innerResult.setFutureResolution(ImmutableMap.builder() + .put(TimeTypeConstants.END_DATE, + DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getFutureValue())) + .build()); + + innerResult.setPastResolution(ImmutableMap.builder() + .put(TimeTypeConstants.END_DATE, + DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getPastValue())) + .build()); + } else if (innerResult.getMod() != null && innerResult.getMod().equals(Constants.AFTER_MOD)) { + innerResult.setFutureResolution(ImmutableMap.builder() + .put(TimeTypeConstants.START_DATE, + DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getFutureValue())) + .build()); + + innerResult.setPastResolution(ImmutableMap.builder() + .put(TimeTypeConstants.START_DATE, + DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getPastValue())) + .build()); + } else if (innerResult.getFutureValue() != null && innerResult.getPastValue() != null) { + innerResult.setFutureResolution(ImmutableMap.builder() + .put(TimeTypeConstants.START_DATE, + DateTimeFormatUtil.formatDate(((Pair)innerResult.getFutureValue()).getValue0())) + .put(TimeTypeConstants.END_DATE, + DateTimeFormatUtil.formatDate(((Pair)innerResult.getFutureValue()).getValue1())) + .build()); + + innerResult.setPastResolution(ImmutableMap.builder() + .put(TimeTypeConstants.START_DATE, + DateTimeFormatUtil.formatDate(((Pair)innerResult.getPastValue()).getValue0())) + .put(TimeTypeConstants.END_DATE, + DateTimeFormatUtil.formatDate(((Pair)innerResult.getPastValue()).getValue1())) + .build()); + } else { + innerResult.setFutureResolution(new HashMap<>()); + innerResult.setPastResolution(new HashMap<>()); + } + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult(er.getStart(), er.getLength(), er.getText(), er.getType(), er.getData(), value, "", "", er.getMetadata()); + + if (value != null) { + ret.setTimexStr(value.getTimex()); + } + + return ret; + } + + // Process case like "from|between START to|and END" where START/END can be dateRange or datePoint + private DateTimeResolutionResult parseComplexDatePeriod(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getComplexDatePeriodRegex(), text)).findFirst(); + + if (match.isPresent()) { + LocalDateTime futureBegin = LocalDateTime.MIN; + LocalDateTime futureEnd = LocalDateTime.MIN; + LocalDateTime pastBegin = LocalDateTime.MIN; + LocalDateTime pastEnd = LocalDateTime.MIN; + boolean isSpecificDate = false; + boolean isStartByWeek = false; + boolean isEndByWeek = false; + DateContext dateContext = getYearContext(match.get().getGroup("start").value.trim(), match.get().getGroup("end").value.trim(), text); + + DateTimeResolutionResult startResolution = parseSingleTimePoint(match.get().getGroup("start").value.trim(), referenceDate, dateContext); + + if (startResolution.getSuccess()) { + futureBegin = (LocalDateTime)startResolution.getFutureValue(); + pastBegin = (LocalDateTime)startResolution.getPastValue(); + isSpecificDate = true; + } else { + startResolution = parseBaseDatePeriod(match.get().getGroup("start").value.trim(), referenceDate, dateContext); + if (startResolution.getSuccess()) { + futureBegin = ((Pair)startResolution.getFutureValue()).getValue0(); + pastBegin = ((Pair)startResolution.getPastValue()).getValue0(); + + if (startResolution.getTimex().contains("-W")) { + isStartByWeek = true; + } + } + } + + if (startResolution.getSuccess()) { + DateTimeResolutionResult endResolution = parseSingleTimePoint(match.get().getGroup("end").value.trim(), referenceDate, dateContext); + + if (endResolution.getSuccess()) { + futureEnd = (LocalDateTime)endResolution.getFutureValue(); + pastEnd = (LocalDateTime)endResolution.getPastValue(); + isSpecificDate = true; + } else { + endResolution = parseBaseDatePeriod(match.get().getGroup("end").value.trim(), referenceDate, dateContext); + + if (endResolution.getSuccess()) { + futureEnd = ((Pair)endResolution.getFutureValue()).getValue0(); + pastEnd = ((Pair)endResolution.getPastValue()).getValue0(); + + if (endResolution.getTimex().contains("-W")) { + isEndByWeek = true; + } + } + } + + if (endResolution.getSuccess()) { + if (futureBegin.isAfter(futureEnd)) { + if (dateContext == null || dateContext.isEmpty()) { + futureBegin = pastBegin; + } else { + futureBegin = dateContext.swiftDateObject(futureBegin, futureEnd); + } + } + + if (pastEnd.isBefore(pastBegin)) { + if (dateContext == null || dateContext.isEmpty()) { + pastEnd = futureEnd; + } else { + pastBegin = dateContext.swiftDateObject(pastBegin, pastEnd); + } + } + + // If both begin/end are date ranges in "Month", the Timex should be ByMonth + // The year period case should already be handled in Basic Cases + DatePeriodTimexType datePeriodTimexType = DatePeriodTimexType.ByMonth; + + if (isSpecificDate) { + // If at least one of the begin/end is specific date, the Timex should be ByDay + datePeriodTimexType = DatePeriodTimexType.ByDay; + } else if (isStartByWeek && isEndByWeek) { + // If both begin/end are date ranges in "Week", the Timex should be ByWeek + datePeriodTimexType = DatePeriodTimexType.ByWeek; + } + + ret.setTimex(TimexUtility.generateDatePeriodTimex(futureBegin, futureEnd, datePeriodTimexType, pastBegin, pastEnd)); + + ret.setFutureValue(new Pair<>(futureBegin, futureEnd)); + ret.setPastValue(new Pair<>(pastBegin, pastEnd)); + ret.setSuccess(true); + } + } + } + return ret; + } + + private DateTimeResolutionResult parseBaseDatePeriod(String text, LocalDateTime referenceDate) { + return parseBaseDatePeriod(text, referenceDate, null); + } + + private DateTimeResolutionResult parseBaseDatePeriod(String text, LocalDateTime referenceDate, DateContext dateContext) { + DateTimeResolutionResult innerResult = parseMonthWithYear(text, referenceDate); + + if (!innerResult.getSuccess()) { + innerResult = parseSimpleCases(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseOneWordPeriod(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = mergeTwoTimePoints(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseYear(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseWeekOfMonth(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseWeekOfYear(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseHalfYear(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseQuarter(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseSeason(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseWhichWeek(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseWeekOfDate(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseMonthOfDate(text, referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = parseDecade(text, referenceDate); + } + + // Cases like "within/less than/more than x weeks from/before/after today" + if (!innerResult.getSuccess()) { + innerResult = parseDatePointWithAgoAndLater(text, referenceDate); + } + + // Parse duration should be at the end since it will extract "the last week" from "the last week of July" + if (!innerResult.getSuccess()) { + innerResult = parseDuration(text, referenceDate); + } + + // Cases like "21st century" + if (!innerResult.getSuccess()) { + innerResult = parseOrdinalNumberWithCenturySuffix(text, referenceDate); + } + + if (innerResult.getSuccess() && dateContext != null) { + innerResult = dateContext.processDatePeriodEntityResolution(innerResult); + } + + return innerResult; + } + + private DateTimeResolutionResult parseOrdinalNumberWithCenturySuffix(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + Optional er = this.config.getOrdinalExtractor().extract(text).stream().findFirst(); + + if (er.isPresent() && er.get().getStart() + er.get().getLength() < text.length()) { + String afterString = text.substring(er.get().getStart() + er.get().getLength()).trim(); + + // It falls into the cases like "21st century" + if (Arrays.stream(RegExpUtility.getMatches(this.config.getCenturySuffixRegex(), afterString)).findFirst().isPresent()) { + ParseResult number = this.config.getNumberParser().parse(er.get()); + + if (number.getValue() != null) { + // Note that 1st century means from year 0 - 100 + int startYear = (Math.round(((Double)number.getValue()).floatValue()) - 1) * Constants.CenturyYearsCount; + LocalDateTime startDate = DateUtil.safeCreateFromMinValue(startYear, 1, 1); + LocalDateTime endDate = DateUtil.safeCreateFromMinValue(startYear + Constants.CenturyYearsCount, 1, 1); + + String startLuisStr = DateTimeFormatUtil.luisDate(startDate); + String endLuisStr = DateTimeFormatUtil.luisDate(endDate); + String durationTimex = "P" + Constants.CenturyYearsCount + "Y"; + + ret.setTimex(String.format("(%s,%s,%s)", startLuisStr, endLuisStr, durationTimex)); + ret.setFutureValue(new Pair<>(startDate, endDate)); + ret.setPastValue(new Pair<>(startDate, endDate)); + ret.setSuccess(true); + } + } + } + + return ret; + } + + private DateTimeResolutionResult parseDatePointWithAgoAndLater(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + Optional er = this.config.getDateExtractor().extract(text, referenceDate).stream().findFirst(); + + if (er.isPresent()) { + String beforeString = text.substring(0, er.get().getStart()); + boolean isAgo = Arrays.stream(RegExpUtility.getMatches(this.config.getAgoRegex(), er.get().getText())).findFirst().isPresent(); + boolean isLater = Arrays.stream(RegExpUtility.getMatches(this.config.getLaterRegex(), er.get().getText())).findFirst().isPresent(); + + if (!StringUtility.isNullOrEmpty(beforeString) && (isAgo || isLater)) { + boolean isLessThanOrWithIn = false; + boolean isMoreThan = false; + + // cases like "within 3 days from yesterday/tomorrow" does not make any sense + if (er.get().getText().contains("today") || er.get().getText().contains("now")) { + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getWithinNextPrefixRegex(), beforeString)).findFirst(); + if (match.isPresent()) { + boolean isNext = !StringUtility.isNullOrEmpty(match.get().getGroup("next").value); + + // cases like "within the next 5 days before today" is not acceptable + if (!(isNext && isAgo)) { + isLessThanOrWithIn = true; + } + } + } + + isLessThanOrWithIn = isLessThanOrWithIn || (Arrays.stream(RegExpUtility.getMatches(this.config.getLessThanRegex(), beforeString)).findFirst().isPresent()); + isMoreThan = Arrays.stream(RegExpUtility.getMatches(this.config.getMoreThanRegex(), beforeString)).findFirst().isPresent(); + + DateTimeParseResult pr = this.config.getDateParser().parse(er.get(), referenceDate); + Optional durationExtractionResult = this.config.getDurationExtractor().extract(er.get().getText()).stream().findFirst(); + + if (durationExtractionResult.isPresent()) { + ParseResult duration = this.config.getDurationParser().parse(durationExtractionResult.get()); + long durationInSeconds = Math.round((Double)((DateTimeResolutionResult)(duration.getValue())).getPastValue()); + + if (isLessThanOrWithIn) { + LocalDateTime startDate; + LocalDateTime endDate; + + if (isAgo) { + startDate = (LocalDateTime)((DateTimeResolutionResult)(pr.getValue())).getPastValue(); + endDate = startDate.plusSeconds(durationInSeconds); + } else { + endDate = (LocalDateTime)((DateTimeResolutionResult)(pr.getValue())).getFutureValue(); + startDate = endDate.minusSeconds(durationInSeconds); + } + + if (startDate != LocalDateTime.MIN) { + String startLuisStr = DateTimeFormatUtil.luisDate(startDate); + String endLuisStr = DateTimeFormatUtil.luisDate(endDate); + String durationTimex = ((DateTimeResolutionResult)(duration.getValue())).getTimex(); + + ret.setTimex(String.format("(%s,%s,%s)", startLuisStr, endLuisStr, durationTimex)); + ret.setFutureValue(new Pair<>(startDate, endDate)); + ret.setPastValue(new Pair<>(startDate, endDate)); + ret.setSuccess(true); + } + } else if (isMoreThan) { + ret.setMod(isAgo ? Constants.BEFORE_MOD : Constants.AFTER_MOD); + + ret.setTimex(pr.getTimexStr()); + ret.setFutureValue(((DateTimeResolutionResult)(pr.getValue())).getFutureValue()); + ret.setPastValue(((DateTimeResolutionResult)(pr.getValue())).getPastValue()); + ret.setSuccess(true); + } + } + } + } + + return ret; + } + + private DateTimeResolutionResult parseSingleTimePoint(String text, LocalDateTime referenceDate, DateContext dateContext) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + ExtractResult er = this.config.getDateExtractor().extract(text, referenceDate).stream().findFirst().orElse(null); + + if (er != null) { + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getWeekWithWeekDayRangeRegex(), text)).findFirst(); + String weekPrefix = null; + if (match.isPresent()) { + weekPrefix = match.get().getGroup("week").value; + } + + if (!StringUtility.isNullOrEmpty(weekPrefix)) { + er.setText(weekPrefix + " " + er.getText()); + } + + ParseResult pr = this.config.getDateParser().parse(er, referenceDate); + + if (pr != null) { + ret.setTimex("(" + ((DateTimeParseResult)pr).getTimexStr()); + ret.setFutureValue(((DateTimeResolutionResult)pr.getValue()).getFutureValue()); + ret.setPastValue(((DateTimeResolutionResult)pr.getValue()).getPastValue()); + ret.setSuccess(true); + } + + if (dateContext != null) { + ret = dateContext.processDateEntityResolution(ret); + } + } + + return ret; + } + + private DateTimeResolutionResult parseSimpleCases(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + int year = referenceDate.getYear(); + int month = referenceDate.getMonthValue(); + int beginDay; + int endDay; + boolean noYear = true; + + ConditionalMatch match = RegexExtension.matchExact(this.config.getMonthFrontBetweenRegex(), text, true); + String beginLuisStr; + String endLuisStr; + + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getBetweenRegex(), text, true); + } + + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getMonthFrontSimpleCasesRegex(), text, true); + } + + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getSimpleCasesRegex(), text, true); + } + + if (match.getSuccess()) { + MatchGroup days = match.getMatch().get().getGroup("day"); + beginDay = this.config.getDayOfMonth().get(days.captures[0].value.toLowerCase()); + endDay = this.config.getDayOfMonth().get(days.captures[1].value.toLowerCase()); + + // parse year + year = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(match.getMatch().get()); + if (year != Constants.InvalidYear) { + noYear = false; + } else { + year = referenceDate.getYear(); + } + + String monthStr = match.getMatch().get().getGroup("month").value; + if (!StringUtility.isNullOrEmpty(monthStr)) { + month = this.config.getMonthOfYear().get(monthStr.toLowerCase()); + } else { + monthStr = match.getMatch().get().getGroup("relmonth").value.trim().toLowerCase(); + int swiftMonth = this.config.getSwiftDayOrMonth(monthStr); + switch (swiftMonth) { + case 1: + if (month != 12) { + month += 1; + } else { + month = 1; + year += 1; + } + break; + case -1: + if (month != 1) { + month -= 1; + } else { + month = 12; + year -= 1; + } + break; + default: + break; + } + + if (this.config.isFuture(monthStr)) { + noYear = false; + } + } + } else { + return ret; + } + + if (noYear) { + beginLuisStr = DateTimeFormatUtil.luisDate(-1, month, beginDay); + endLuisStr = DateTimeFormatUtil.luisDate(-1, month, endDay); + } else { + beginLuisStr = DateTimeFormatUtil.luisDate(year, month, beginDay); + endLuisStr = DateTimeFormatUtil.luisDate(year, month, endDay); + } + + int futureYear = year; + int pastYear = year; + LocalDateTime startDate = DateUtil.safeCreateFromMinValue(year, month, beginDay); + + if (noYear && startDate.isBefore(referenceDate)) { + futureYear++; + } + + if (noYear && (startDate.isAfter(referenceDate) || startDate.isEqual(referenceDate))) { + pastYear--; + } + + HashMap futurePastBeginDates = DateContext.generateDates(noYear, referenceDate, year, month, beginDay); + HashMap futurePastEndDates = DateContext.generateDates(noYear, referenceDate, year, month, endDay); + + ret.setTimex(String.format("(%s,%s,P%sD)", beginLuisStr, endLuisStr, (endDay - beginDay))); + ret.setFutureValue(new Pair<>(futurePastBeginDates.get(Constants.FutureDate), + futurePastEndDates.get(Constants.FutureDate))); + ret.setPastValue(new Pair<>(futurePastBeginDates.get(Constants.PastDate), + futurePastEndDates.get(Constants.PastDate))); + ret.setSuccess(true); + + return ret; + } + + private boolean isPresent(int swift) { + return swift == 0; + } + + private DateTimeResolutionResult parseOneWordPeriod(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + int year = referenceDate.getYear(); + int month = referenceDate.getMonthValue(); + int futureYear = year; + int pastYear = year; + boolean earlyPrefix = false; + boolean latePrefix = false; + boolean midPrefix = false; + boolean isRef = false; + + boolean earlierPrefix = false; + boolean laterPrefix = false; + + String trimmedText = text.trim().toLowerCase(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getOneWordPeriodRegex(), trimmedText, true); + + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getLaterEarlyPeriodRegex(), trimmedText, true); + } + + // For cases "that week|month|year" + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getReferenceDatePeriodRegex(), trimmedText, true); + isRef = true; + ret.setMod(Constants.REF_UNDEF_MOD); + } + + if (match.getSuccess()) { + if (!match.getMatch().get().getGroup("EarlyPrefix").value.equals("")) { + earlyPrefix = true; + trimmedText = match.getMatch().get().getGroup(Constants.SuffixGroupName).value; + ret.setMod(Constants.EARLY_MOD); + } else if (!match.getMatch().get().getGroup("LatePrefix").value.equals("")) { + latePrefix = true; + trimmedText = match.getMatch().get().getGroup(Constants.SuffixGroupName).value; + ret.setMod(Constants.LATE_MOD); + } else if (!match.getMatch().get().getGroup("MidPrefix").value.equals("")) { + midPrefix = true; + trimmedText = match.getMatch().get().getGroup(Constants.SuffixGroupName).value; + ret.setMod(Constants.MID_MOD); + } + + int swift = 0; + String monthStr = match.getMatch().get().getGroup("month").value; + if (!StringUtility.isNullOrEmpty(monthStr)) { + swift = this.config.getSwiftYear(trimmedText); + } else { + swift = this.config.getSwiftDayOrMonth(trimmedText); + } + + // Handle the abbreviation of DatePeriod, e.g., 'eoy(end of year)', the behavior of 'eoy' should be the same as 'end of year' + Optional unspecificEndOfRangeMatch = Arrays.stream(RegExpUtility.getMatches(config.getUnspecificEndOfRangeRegex(), match.getMatch().get().value)).findFirst(); + if (unspecificEndOfRangeMatch.isPresent()) { + latePrefix = true; + trimmedText = match.getMatch().get().value; + ret.setMod(Constants.LATE_MOD); + } + + if (!match.getMatch().get().getGroup("RelEarly").value.equals("")) { + earlierPrefix = true; + if (isPresent(swift)) { + ret.setMod(null); + } + } else if (!match.getMatch().get().getGroup("RelLate").value.equals("")) { + laterPrefix = true; + if (isPresent(swift)) { + ret.setMod(null); + } + } + + if (this.config.isYearToDate(trimmedText)) { + ret.setTimex(String.format("%04d", referenceDate.getYear())); + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromMinValue(referenceDate.getYear(), 1, 1), referenceDate)); + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromMinValue(referenceDate.getYear(), 1, 1), referenceDate)); + + ret.setSuccess(true); + return ret; + } + + if (this.config.isMonthToDate(trimmedText)) { + ret.setTimex(String.format("%04d-%02d", referenceDate.getYear(), referenceDate.getMonthValue())); + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromMinValue(referenceDate.getYear(), referenceDate.getMonthValue(), 1), + referenceDate)); + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromMinValue(referenceDate.getYear(), referenceDate.getMonthValue(), 1), + referenceDate)); + + ret.setSuccess(true); + return ret; + } + + if (!StringUtility.isNullOrEmpty(monthStr)) { + swift = this.config.getSwiftYear(trimmedText); + + month = this.config.getMonthOfYear().get(monthStr.toLowerCase()); + + if (swift >= -1) { + ret.setTimex(String.format("%04d-%02d", referenceDate.getYear() + swift, month)); + year = year + swift; + futureYear = pastYear = year; + } else { + ret.setTimex(String.format("XXXX-%02d", month)); + if (month < referenceDate.getMonthValue()) { + futureYear++; + } + + if (month >= referenceDate.getMonthValue()) { + pastYear--; + } + } + } else { + swift = this.config.getSwiftDayOrMonth(trimmedText); + + if (this.config.isWeekOnly(trimmedText)) { + LocalDateTime thursday = DateUtil.thisDate(referenceDate, DayOfWeek.THURSDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + + ret.setTimex(isRef ? TimexUtility.generateWeekTimex() : TimexUtility.generateWeekTimex(thursday)); + + LocalDateTime beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.MONDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + + LocalDateTime endValue = DateUtil.thisDate(referenceDate, DayOfWeek.SUNDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + + LocalDateTime endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + + if (earlyPrefix) { + endValue = DateUtil.thisDate(referenceDate, DayOfWeek.WEDNESDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + } else if (midPrefix) { + beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.TUESDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + endValue = DateUtil.thisDate(referenceDate, DayOfWeek.FRIDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + } else if (latePrefix) { + beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.THURSDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + } + + if (earlierPrefix && swift == 0) { + if (endDate.isAfter(referenceDate)) { + endDate = referenceDate; + } + } else if (laterPrefix && swift == 0) { + if (beginDate.isBefore(referenceDate)) { + beginDate = referenceDate; + } + } + + if (latePrefix && swift != 0) { + ret.setMod(Constants.LATE_MOD); + } + + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + + ret.setSuccess(true); + return ret; + } + + if (this.config.isWeekend(trimmedText)) { + LocalDateTime beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.SATURDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + LocalDateTime endValue = DateUtil.thisDate(referenceDate, DayOfWeek.SUNDAY.getValue()).plusDays(Constants.WeekDayCount * swift); + + ret.setTimex(isRef ? TimexUtility.generateWeekendTimex() : TimexUtility.generateWeekendTimex(beginDate)); + + LocalDateTime endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + + ret.setSuccess(true); + return ret; + } + + if (this.config.isMonthOnly(trimmedText)) { + LocalDateTime date = referenceDate.plusMonths(swift); + month = date.getMonthValue(); + year = date.getYear(); + + ret.setTimex(isRef ? TimexUtility.generateMonthTimex() : TimexUtility.generateMonthTimex(date)); + + futureYear = pastYear = year; + } else if (this.config.isYearOnly(trimmedText)) { + LocalDateTime date = referenceDate.plusYears(swift); + year = date.getYear(); + + if (!StringUtility.isNullOrEmpty(match.getMatch().get().getGroup("special").value)) { + String specialYearPrefixes = this.config.getSpecialYearPrefixesMap().get(match.getMatch().get().getGroup("special").value.toLowerCase()); + swift = this.config.getSwiftYear(trimmedText); + year = swift < -1 ? Constants.InvalidYear : year; + ret.setTimex(TimexUtility.generateYearTimex(year, specialYearPrefixes)); + ret.setSuccess(true); + return ret; + } + + LocalDateTime beginDate = DateUtil.safeCreateFromMinValue(year, 1, 1); + + LocalDateTime endValue = DateUtil.safeCreateFromMinValue(year, 12, 31); + LocalDateTime endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + + if (earlyPrefix) { + endValue = DateUtil.safeCreateFromMinValue(year, 6, 30); + endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + } else if (midPrefix) { + beginDate = DateUtil.safeCreateFromMinValue(year, 4, 1); + endValue = DateUtil.safeCreateFromMinValue(year, 9, 30); + endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1); + } else if (latePrefix) { + beginDate = DateUtil.safeCreateFromMinValue(year, 7, 1); + } + + if (earlierPrefix && swift == 0) { + if (endDate.isAfter(referenceDate)) { + endDate = referenceDate; + } + } else if (laterPrefix && swift == 0) { + if (beginDate.isBefore(referenceDate)) { + beginDate = referenceDate; + } + } + + year = isRef ? Constants.InvalidYear : year; + ret.setTimex(TimexUtility.generateYearTimex(year)); + + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + + ret.setSuccess(true); + return ret; + } + } + } else { + return ret; + } + + // only "month" will come to here + LocalDateTime futureStart = DateUtil.safeCreateFromMinValue(futureYear, month, 1); + LocalDateTime futureEnd = inclusiveEndPeriod ? futureStart.plusMonths(1).minusDays(1) : futureStart.plusMonths(1); + + + LocalDateTime pastStart = DateUtil.safeCreateFromMinValue(pastYear, month, 1); + LocalDateTime pastEnd = inclusiveEndPeriod ? pastStart.plusMonths(1).minusDays(1) : pastStart.plusMonths(1); + + if (earlyPrefix) { + futureEnd = inclusiveEndPeriod ? + DateUtil.safeCreateFromMinValue(futureYear, month, 15) : + DateUtil.safeCreateFromMinValue(futureYear, month, 15).plusDays(1); + pastEnd = inclusiveEndPeriod ? + DateUtil.safeCreateFromMinValue(pastYear, month, 15) : + DateUtil.safeCreateFromMinValue(pastYear, month, 15).plusDays(1); + } else if (midPrefix) { + futureStart = DateUtil.safeCreateFromMinValue(futureYear, month, 10); + pastStart = DateUtil.safeCreateFromMinValue(pastYear, month, 10); + futureEnd = inclusiveEndPeriod ? + DateUtil.safeCreateFromMinValue(futureYear, month, 20) : + DateUtil.safeCreateFromMinValue(futureYear, month, 20).plusDays(1); + pastEnd = inclusiveEndPeriod ? + DateUtil.safeCreateFromMinValue(pastYear, month, 20) : + DateUtil.safeCreateFromMinValue(pastYear, month, 20).plusDays(1); + } else if (latePrefix) { + futureStart = DateUtil.safeCreateFromMinValue(futureYear, month, 16); + pastStart = DateUtil.safeCreateFromMinValue(pastYear, month, 16); + } + + if (earlierPrefix && futureEnd.isEqual(pastEnd)) { + if (futureEnd.isAfter(referenceDate)) { + futureEnd = pastEnd = referenceDate; + } + } else if (laterPrefix && futureStart.isEqual(pastStart)) { + if (futureStart.isBefore(referenceDate)) { + futureStart = pastStart = referenceDate; + } + } + + ret.setFutureValue(new Pair<>(futureStart, futureEnd)); + + ret.setPastValue(new Pair<>(pastStart, pastEnd)); + + ret.setSuccess(true); + + return ret; + } + + private DateTimeResolutionResult parseMonthWithYear(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + ConditionalMatch match = RegexExtension.matchExact(this.config.getMonthWithYear(), text, true); + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getMonthNumWithYear(), text, true); + } + + if (match.getSuccess()) { + String monthStr = match.getMatch().get().getGroup("month").value.toLowerCase(); + String orderStr = match.getMatch().get().getGroup("order").value.toLowerCase(); + + int month = this.config.getMonthOfYear().get(monthStr.toLowerCase()); + + int year = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(match.getMatch().get()); + if (year == Constants.InvalidYear) { + int swift = this.config.getSwiftYear(orderStr); + if (swift < -1) { + return ret; + } + year = referenceDate.getYear() + swift; + } + + LocalDateTime startValue = DateUtil.safeCreateFromMinValue(year, month, 1); + LocalDateTime endValue = inclusiveEndPeriod ? + DateUtil.safeCreateFromMinValue(year, month, 1).plusMonths(1).minusDays(1) : + DateUtil.safeCreateFromMinValue(year, month, 1).plusMonths(1); + + ret.setFutureValue(new Pair<>(startValue, endValue)); + ret.setPastValue(new Pair<>(startValue, endValue)); + + ret.setTimex(String.format("%04d-%02d", year, month)); + + ret.setSuccess(true); + } + + return ret; + } + + private DateTimeResolutionResult parseYear(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + int year = Constants.InvalidYear; + + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getYearPeriodRegex(), text)).findFirst(); + Optional matchMonth = Arrays.stream(RegExpUtility.getMatches(this.config.getMonthWithYear(), text)).findFirst(); + ; + + if (match.isPresent() && !matchMonth.isPresent()) { + int beginYear = Constants.InvalidYear; + int endYear = Constants.InvalidYear; + + Match[] matches = RegExpUtility.getMatches(this.config.getYearRegex(), text); + if (matches.length == 2) { + // (from|during|in|between)? 2012 (till|to|until|through|-) 2015 + if (!matches[0].value.equals("")) { + beginYear = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(matches[0]); + if (!(beginYear >= Constants.MinYearNum && beginYear <= Constants.MaxYearNum)) { + beginYear = Constants.InvalidYear; + } + } + + if (!matches[1].value.equals("")) { + endYear = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(matches[1]); + if (!(endYear >= Constants.MinYearNum && endYear <= Constants.MaxYearNum)) { + endYear = Constants.InvalidYear; + } + } + } + + if (beginYear != Constants.InvalidYear && endYear != Constants.InvalidYear) { + LocalDateTime beginDay = DateUtil.safeCreateFromMinValue(beginYear, 1, 1); + + LocalDateTime endDayValue = DateUtil.safeCreateFromMinValue(endYear, 1, 1); + LocalDateTime endDay = inclusiveEndPeriod ? endDayValue.minusDays(1) : endDayValue; + + ret.setTimex(String.format("(%s,%s,P%sY)", DateTimeFormatUtil.luisDate(beginDay), DateTimeFormatUtil.luisDate(endDay), (endYear - beginYear))); + ret.setFutureValue(new Pair<>(beginDay, endDay)); + ret.setPastValue(new Pair<>(beginDay, endDay)); + ret.setSuccess(true); + + return ret; + } + } else { + ConditionalMatch exactMatch = RegexExtension.matchExact(this.config.getYearRegex(), text, true); + if (exactMatch.getSuccess()) { + year = this.config.getDateExtractor().getYearFromText(exactMatch.getMatch().get()); + if (!(year >= Constants.MinYearNum && year <= Constants.MaxYearNum)) { + year = Constants.InvalidYear; + } + } else { + exactMatch = RegexExtension.matchExact(this.config.getYearPlusNumberRegex(), text, true); + if (exactMatch.getSuccess()) { + year = this.config.getDateExtractor().getYearFromText(exactMatch.getMatch().get()); + if (!StringUtility.isNullOrEmpty(exactMatch.getMatch().get().getGroup("special").value)) { + String specialYearPrefixes = this.config.getSpecialYearPrefixesMap().get(exactMatch.getMatch().get().getGroup("special").value.toLowerCase()); + ret.setTimex(TimexUtility.generateYearTimex(year, specialYearPrefixes)); + ret.setSuccess(true); + return ret; + } + } + } + + if (year != Constants.InvalidYear) { + LocalDateTime beginDay = DateUtil.safeCreateFromMinValue(year, 1, 1); + + LocalDateTime endDayValue = DateUtil.safeCreateFromMinValue(year + 1, 1, 1); + LocalDateTime endDay = inclusiveEndPeriod ? endDayValue.minusDays(1) : endDayValue; + + ret.setTimex(TimexUtility.generateYearTimex(year)); + ret.setFutureValue(new Pair<>(beginDay, endDay)); + ret.setPastValue(new Pair<>(beginDay, endDay)); + ret.setSuccess(true); + + return ret; + } + } + + return ret; + } + + // parse entities that made up by two time points + private DateTimeResolutionResult mergeTwoTimePoints(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + List er = this.config.getDateExtractor().extract(text, referenceDate); + DateTimeParseResult pr1 = null; + DateTimeParseResult pr2 = null; + if (er.size() < 2) { + er = this.config.getDateExtractor().extract(this.config.getTokenBeforeDate() + text, referenceDate); + if (er.size() >= 2) { + er.get(0).setStart(er.get(0).getStart() - this.config.getTokenBeforeDate().length()); + er.get(1).setStart(er.get(1).getStart() - this.config.getTokenBeforeDate().length()); + er.set(0, er.get(0)); + er.set(1, er.get(1)); + } else { + DateTimeParseResult nowPr = parseNowAsDate(text, referenceDate); + if (nowPr == null || er.size() < 1) { + return ret; + } + + DateTimeParseResult datePr = this.config.getDateParser().parse(er.get(0), referenceDate); + pr1 = datePr.getStart() < nowPr.getStart() ? datePr : nowPr; + pr2 = datePr.getStart() < nowPr.getStart() ? nowPr : datePr; + } + + } + if (er.size() >= 2) { + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getWeekWithWeekDayRangeRegex(), text)).findFirst(); + String weekPrefix = null; + if (match.isPresent()) { + weekPrefix = match.get().getGroup("week").value; + } + + if (!StringUtility.isNullOrEmpty(weekPrefix)) { + er.get(0).setText(String.format("%s %s", weekPrefix, er.get(0).getText())); + er.get(1).setText(String.format("%s %s", weekPrefix, er.get(1).getText())); + er.set(0, er.get(0)); + er.set(1, er.get(1)); + } + + DateContext dateContext = getYearContext(er.get(0).getText(), er.get(1).getText(), text); + + pr1 = this.config.getDateParser().parse(er.get(0), referenceDate); + pr2 = this.config.getDateParser().parse(er.get(1), referenceDate); + + if (pr1.getValue() == null || pr2.getValue() == null) { + return ret; + } + + pr1 = dateContext.processDateEntityParsingResult(pr1); + pr2 = dateContext.processDateEntityParsingResult(pr2); + + // When the case has no specified year, we should sync the future/past year due to invalid date Feb 29th. + if (dateContext.isEmpty() && (DateContext.isFeb29th((LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getFutureValue()) || + DateContext.isFeb29th((LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getFutureValue()))) { + + HashMap parseResultHashMap = dateContext.syncYear(pr1, pr2); + pr1 = parseResultHashMap.get(Constants.ParseResult1); + pr2 = parseResultHashMap.get(Constants.ParseResult2); + } + } + + List subDateTimeEntities = new ArrayList(); + subDateTimeEntities.add(pr1); + subDateTimeEntities.add(pr2); + ret.setSubDateTimeEntities(subDateTimeEntities); + + LocalDateTime futureBegin = (LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getFutureValue(); + LocalDateTime futureEnd = (LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getFutureValue(); + + LocalDateTime pastBegin = (LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getPastValue(); + LocalDateTime pastEnd = (LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getPastValue(); + + if (futureBegin.isAfter(futureEnd)) { + futureBegin = pastBegin; + } + + if (pastEnd.isBefore(pastBegin)) { + pastEnd = futureEnd; + } + + ret.setTimex(TimexUtility.generateDatePeriodTimexStr(futureBegin, futureEnd, DatePeriodTimexType.ByDay, pr1.getTimexStr(), pr2.getTimexStr())); + + if (pr1.getTimexStr().startsWith(Constants.TimexFuzzyYear) && futureBegin.compareTo(DateUtil.safeCreateFromMinValue(futureBegin.getYear(), 2, 28)) <= 0 && + futureEnd.compareTo(DateUtil.safeCreateFromMinValue(futureBegin.getYear(), 3, 1)) >= 0) { + + // Handle cases like "Feb 29th to March 1st". + // There may be different timexes for FutureValue and PastValue due to the different validity of Feb 29th. + ret.setComment(Constants.Comment_DoubleTimex); + String pastTimex = TimexUtility.generateDatePeriodTimexStr(pastBegin, pastEnd, DatePeriodTimexType.ByDay, pr1.getTimexStr(), pr2.getTimexStr()); + ret.setTimex(TimexUtility.mergeTimexAlternatives(ret.getTimex(), pastTimex)); + } + ret.setFutureValue(new Pair<>(futureBegin, futureEnd)); + ret.setPastValue(new Pair<>(pastBegin, pastEnd)); + ret.setSuccess(true); + + return ret; + } + + // parse entities that made up by two time points with now + private DateTimeParseResult parseNowAsDate(String text, LocalDateTime referenceDate) { + DateTimeParseResult nowPr = null; + LocalDateTime value = referenceDate.toLocalDate().atStartOfDay(); + Match[] matches = RegExpUtility.getMatches(this.config.getNowRegex(), text); + for (Match match : matches) { + DateTimeResolutionResult retNow = new DateTimeResolutionResult(); + retNow.setTimex(DateTimeFormatUtil.luisDate(value)); + retNow.setFutureValue(value); + retNow.setPastValue(value); + + nowPr = new DateTimeParseResult( + match.index, + match.length, + match.value, + Constants.SYS_DATETIME_DATE, + null, + retNow, + "", + value == null ? "" : ((DateTimeResolutionResult)retNow).getTimex()); + } + + return nowPr; + } + + private DateTimeResolutionResult parseDuration(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + LocalDateTime beginDate = referenceDate; + LocalDateTime endDate = referenceDate; + String timex = ""; + boolean restNowSunday = false; + List dateList = null; + + List durationErs = config.getDurationExtractor().extract(text, referenceDate); + if (durationErs.size() == 1) { + ParseResult durationPr = config.getDurationParser().parse(durationErs.get(0)); + String beforeStr = text.substring(0, (durationPr.getStart() != null) ? durationPr.getStart() : 0).trim().toLowerCase(); + String afterStr = text.substring( + ((durationPr.getStart() != null) ? durationPr.getStart() : 0) + ((durationPr.getLength() != null) ? durationPr.getLength() : 0)) + .trim().toLowerCase(); + + List numbersInSuffix = config.getCardinalExtractor().extract(beforeStr); + List numbersInDuration = config.getCardinalExtractor().extract(durationErs.get(0).getText()); + + // Handle cases like "2 upcoming days", "5 previous years" + if (!numbersInSuffix.isEmpty() && numbersInDuration.isEmpty()) { + ExtractResult numberEr = numbersInSuffix.stream().findFirst().get(); + String numberText = numberEr.getText(); + String durationText = durationErs.get(0).getText(); + String combinedText = String.format("%s %s", numberText, durationText); + List combinedDurationEr = config.getDurationExtractor().extract(combinedText, referenceDate); + + if (!combinedDurationEr.isEmpty()) { + durationPr = config.getDurationParser().parse(combinedDurationEr.stream().findFirst().get()); + int startIndex = numberEr.getStart() + numberEr.getLength(); + beforeStr = beforeStr.substring(startIndex).trim(); + } + } + + GetModAndDateResult getModAndDateResult = new GetModAndDateResult(); + + if (durationPr.getValue() != null) { + DateTimeResolutionResult durationResult = (DateTimeResolutionResult)durationPr.getValue(); + + if (StringUtility.isNullOrEmpty(durationResult.getTimex())) { + return ret; + } + + Optional prefixMatch = Arrays.stream(RegExpUtility.getMatches(config.getPastRegex(), beforeStr)).findFirst(); + Optional suffixMatch = Arrays.stream(RegExpUtility.getMatches(config.getPastRegex(), afterStr)).findFirst(); + if (prefixMatch.isPresent() || suffixMatch.isPresent()) { + getModAndDateResult = getModAndDate(beginDate, endDate, referenceDate, durationResult.getTimex(), false); + beginDate = getModAndDateResult.beginDate; + } + + // Handle the "within two weeks" case which means from today to the end of next two weeks + // Cases like "within 3 days before/after today" is not handled here (4th condition) + boolean isMatch = false; + if (RegexExtension.isExactMatch(config.getWithinNextPrefixRegex(), beforeStr, true)) { + getModAndDateResult = getModAndDate(beginDate, endDate, referenceDate, durationResult.getTimex(), true); + beginDate = getModAndDateResult.beginDate; + endDate = getModAndDateResult.endDate; + + // In GetModAndDate, this "future" resolution will add one day to beginDate/endDate, but for the "within" case it should start from the current day. + beginDate = beginDate.minusDays(1); + endDate = endDate.minusDays(1); + isMatch = true; + } + + if (RegexExtension.isExactMatch(config.getFutureRegex(), beforeStr, true)) { + getModAndDateResult = getModAndDate(beginDate, endDate, referenceDate, durationResult.getTimex(), true); + beginDate = getModAndDateResult.beginDate; + endDate = getModAndDateResult.endDate; + isMatch = true; + } + + Optional futureSuffixMatch = Arrays.stream(RegExpUtility.getMatches(config.getFutureSuffixRegex(), afterStr)).findFirst(); + if (futureSuffixMatch.isPresent()) { + getModAndDateResult = getModAndDate(beginDate, endDate, referenceDate, durationResult.getTimex(), true); + beginDate = getModAndDateResult.beginDate; + endDate = getModAndDateResult.endDate; + } + + // Handle the "in two weeks" case which means the second week + if (RegexExtension.isExactMatch(config.getInConnectorRegex(), beforeStr, true) && + !DurationParsingUtil.isMultipleDuration(durationResult.getTimex()) && !isMatch) { + getModAndDateResult = getModAndDate(beginDate, endDate, referenceDate, durationResult.getTimex(), true); + beginDate = getModAndDateResult.beginDate; + endDate = getModAndDateResult.endDate; + + // Change the duration value and the beginDate + String unit = durationResult.getTimex().substring(durationResult.getTimex().length() - 1); + + durationResult.setTimex(String.format("P1%s", unit)); + beginDate = DurationParsingUtil.shiftDateTime(durationResult.getTimex(), endDate, false); + } + + if (!StringUtility.isNullOrEmpty(getModAndDateResult.mod)) { + ((DateTimeResolutionResult)durationPr.getValue()).setMod(getModAndDateResult.mod); + } + + timex = durationResult.getTimex(); + + List subDateTimeEntities = new ArrayList<>(); + subDateTimeEntities.add(durationPr); + ret.setSubDateTimeEntities(subDateTimeEntities); + + if (getModAndDateResult.dateList != null) { + ret.setList(getModAndDateResult.dateList.stream().map(e -> (Object)e).collect(Collectors.toList())); + } + } + } + + // Parse "rest of" + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getRestOfDateRegex(), text)).findFirst(); + if (match.isPresent()) { + String durationStr = match.get().getGroup("duration").value; + String durationUnit = this.config.getUnitMap().get(durationStr); + switch (durationUnit) { + case "W": + int diff = Constants.WeekDayCount - ((beginDate.getDayOfWeek().getValue()) == 0 ? Constants.WeekDayCount : beginDate.getDayOfWeek().getValue()); + endDate = beginDate.plusDays(diff); + timex = String.format("P%s%s", diff, Constants.TimexDay); + if (diff == 0) { + restNowSunday = true; + } + break; + + case "MON": + endDate = DateUtil.safeCreateFromMinValue(beginDate.getYear(), beginDate.getMonthValue(), 1); + endDate = endDate.plusMonths(1).minusDays(1); + diff = (int)ChronoUnit.DAYS.between(beginDate, endDate) + 1; + timex = String.format("P%s%s", diff, Constants.TimexDay); + break; + + case "Y": + endDate = DateUtil.safeCreateFromMinValue(beginDate.getYear(), 12, 1); + endDate = endDate.plusMonths(1).minusDays(1); + diff = (int)ChronoUnit.DAYS.between(beginDate, endDate) + 1; + timex = String.format("P%s%s", diff, Constants.TimexDay); + break; + default: + break; + } + } + + if (!beginDate.equals(endDate) || restNowSunday) { + endDate = inclusiveEndPeriod ? endDate.minusDays(1) : endDate; + + ret.setTimex(String.format("(%s,%s,%s)", DateTimeFormatUtil.luisDate(beginDate), DateTimeFormatUtil.luisDate(endDate), timex)); + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + ret.setSuccess(true); + + return ret; + } + + return ret; + } + + private GetModAndDateResult getModAndDate(LocalDateTime beginDate, LocalDateTime endDate, LocalDateTime referenceDate, String timex, boolean future) { + LocalDateTime beginDateResult = beginDate; + LocalDateTime endDateResult = endDate; + boolean isBusinessDay = timex.endsWith(Constants.TimexBusinessDay); + int businessDayCount = 0; + + if (isBusinessDay) { + businessDayCount = Integer.parseInt(timex.substring(1, timex.length() - 2)); + } + + if (future) { + String mod = Constants.AFTER_MOD; + + // For future the beginDate should add 1 first + if (isBusinessDay) { + beginDateResult = DurationParsingUtil.getNextBusinessDay(referenceDate); + NthBusinessDayResult nthBusinessDayResult = DurationParsingUtil.getNthBusinessDay(beginDateResult, businessDayCount - 1, true); + endDateResult = nthBusinessDayResult.result.plusDays(1); + return new GetModAndDateResult(beginDateResult, endDateResult, mod, nthBusinessDayResult.dateList); + } else { + beginDateResult = referenceDate.plusDays(1); + endDateResult = DurationParsingUtil.shiftDateTime(timex, beginDateResult, true); + return new GetModAndDateResult(beginDateResult, endDateResult, mod, null); + } + + } else { + String mod = Constants.BEFORE_MOD; + + if (isBusinessDay) { + endDateResult = DurationParsingUtil.getNextBusinessDay(endDateResult, false); + NthBusinessDayResult nthBusinessDayResult = DurationParsingUtil.getNthBusinessDay(endDateResult, businessDayCount - 1, false); + endDateResult = endDateResult.plusDays(1); + beginDateResult = nthBusinessDayResult.result; + return new GetModAndDateResult(beginDateResult, endDateResult, mod, nthBusinessDayResult.dateList); + } else { + beginDateResult = DurationParsingUtil.shiftDateTime(timex, endDateResult, false); + return new GetModAndDateResult(beginDateResult, endDateResult, mod, null); + } + } + } + + // To be consistency, we follow the definition of "week of year": + // "first week of the month" - it has the month's first Thursday in it + // "last week of the month" - it has the month's last Thursday in it + private DateTimeResolutionResult parseWeekOfMonth(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + String trimmedText = text.trim().toLowerCase(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getWeekOfMonthRegex(), trimmedText, true); + if (!match.getSuccess()) { + return ret; + } + + String cardinalStr = match.getMatch().get().getGroup("cardinal").value; + String monthStr = match.getMatch().get().getGroup("month").value; + boolean noYear = false; + int year; + + int month; + if (StringUtility.isNullOrEmpty(monthStr)) { + int swift = this.config.getSwiftDayOrMonth(trimmedText); + + month = referenceDate.plusMonths(swift).getMonthValue(); + year = referenceDate.plusMonths(swift).getYear(); + } else { + month = this.config.getMonthOfYear().get(monthStr); + year = config.getDateExtractor().getYearFromText(match.getMatch().get()); + + if (year == Constants.InvalidYear) { + year = referenceDate.getYear(); + noYear = true; + } + } + + ret = getWeekOfMonth(cardinalStr, month, year, referenceDate, noYear); + + return ret; + } + + private DateTimeResolutionResult parseWeekOfYear(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + String trimmedText = text.trim().toLowerCase(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getWeekOfYearRegex(), trimmedText, true); + if (!match.getSuccess()) { + return ret; + } + + String cardinalStr = match.getMatch().get().getGroup("cardinal").value; + String orderStr = match.getMatch().get().getGroup("order").value.toLowerCase(); + + int year = this.config.getDateExtractor().getYearFromText(match.getMatch().get()); + if (year == Constants.InvalidYear) { + int swift = this.config.getSwiftYear(orderStr); + if (swift < -1) { + return ret; + } + year = referenceDate.getYear() + swift; + } + + LocalDateTime targetWeekMonday; + if (this.config.isLastCardinal(cardinalStr)) { + targetWeekMonday = DateUtil.thisDate(getLastThursday(year), DayOfWeek.MONDAY.getValue()); + + ret.setTimex(TimexUtility.generateWeekTimex(targetWeekMonday)); + } else { + int weekNum = this.config.getCardinalMap().get(cardinalStr); + targetWeekMonday = DateUtil.thisDate(getFirstThursday(year), DayOfWeek.MONDAY.getValue()) + .plusDays(Constants.WeekDayCount * (weekNum - 1)); + + ret.setTimex(TimexUtility.generateWeekOfYearTimex(year, weekNum)); + } + + ret.setFutureValue(inclusiveEndPeriod ? + new Pair<>(targetWeekMonday, targetWeekMonday.plusDays(Constants.WeekDayCount - 1)) : + new Pair<>(targetWeekMonday, targetWeekMonday.plusDays(Constants.WeekDayCount))); + + ret.setPastValue(inclusiveEndPeriod ? + new Pair<>(targetWeekMonday, targetWeekMonday.plusDays(Constants.WeekDayCount - 1)) : + new Pair<>(targetWeekMonday, targetWeekMonday.plusDays(Constants.WeekDayCount))); + + ret.setSuccess(true); + + return ret; + } + + private DateTimeResolutionResult parseHalfYear(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getAllHalfYearRegex(), text, true); + + if (!match.getSuccess()) { + return ret; + } + + String cardinalStr = match.getMatch().get().getGroup("cardinal").value.toLowerCase(); + String orderStr = match.getMatch().get().getGroup("order").value.toLowerCase(); + String numberStr = match.getMatch().get().getGroup("number").value; + + int year = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(match.getMatch().get()); + + if (year == Constants.InvalidYear) { + int swift = this.config.getSwiftYear(orderStr); + if (swift < -1) { + return ret; + } + year = referenceDate.getYear() + swift; + } + + int halfNum; + if (!StringUtility.isNullOrEmpty(numberStr)) { + halfNum = Integer.parseInt(numberStr); + } else { + halfNum = this.config.getCardinalMap().get(cardinalStr); + } + + LocalDateTime beginDate = DateUtil.safeCreateFromMinValue(year, (halfNum - 1) * Constants.SemesterMonthCount + 1, 1); + LocalDateTime endDate = DateUtil.safeCreateFromMinValue(year, halfNum * Constants.SemesterMonthCount, 1).plusMonths(1); + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + ret.setTimex(String.format("(%s,%s,P6M)", DateTimeFormatUtil.luisDate(beginDate), DateTimeFormatUtil.luisDate(endDate))); + ret.setSuccess(true); + + return ret; + } + + private DateTimeResolutionResult parseQuarter(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getQuarterRegex(), text, true); + + if (!match.getSuccess()) { + match = RegexExtension.matchExact(this.config.getQuarterRegexYearFront(), text, true); + } + + if (!match.getSuccess()) { + return ret; + } + + String cardinalStr = match.getMatch().get().getGroup("cardinal").value.toLowerCase(); + String orderQuarterStr = match.getMatch().get().getGroup("orderQuarter").value.toLowerCase(); + String orderStr = StringUtility.isNullOrEmpty(orderQuarterStr) ? match.getMatch().get().getGroup("order").value.toLowerCase() : null; + String numberStr = match.getMatch().get().getGroup("number").value; + + boolean noSpecificYear = false; + int year = this.config.getDateExtractor().getYearFromText(match.getMatch().get()); + + if (year == Constants.InvalidYear) { + int swift = StringUtility.isNullOrEmpty(orderQuarterStr) ? this.config.getSwiftYear(orderStr) : 0; + if (swift < -1) { + swift = 0; + noSpecificYear = true; + } + year = referenceDate.getYear() + swift; + } + + int quarterNum; + if (!StringUtility.isNullOrEmpty(numberStr)) { + quarterNum = Integer.parseInt(numberStr); + } else if (!StringUtility.isNullOrEmpty(orderQuarterStr)) { + int month = referenceDate.getMonthValue(); + quarterNum = (int)Math.ceil((double)month / Constants.TrimesterMonthCount); + int swift = this.config.getSwiftYear(orderQuarterStr); + quarterNum += swift; + if (quarterNum <= 0) { + quarterNum += Constants.QuarterCount; + year -= 1; + } else if (quarterNum > Constants.QuarterCount) { + quarterNum -= Constants.QuarterCount; + year += 1; + } + } else { + quarterNum = this.config.getCardinalMap().get(cardinalStr); + } + + LocalDateTime beginDate = DateUtil.safeCreateFromMinValue(year, (quarterNum - 1) * Constants.TrimesterMonthCount + 1, 1); + LocalDateTime endDate = DateUtil.safeCreateFromMinValue(year, quarterNum * Constants.TrimesterMonthCount, 1).plusMonths(1); + + if (noSpecificYear) { + if (endDate.compareTo(referenceDate) < 0) { + ret.setPastValue(new Pair<>(beginDate, endDate)); + + LocalDateTime futureBeginDate = DateUtil.safeCreateFromMinValue(year + 1, (quarterNum - 1) * Constants.TrimesterMonthCount + 1, 1); + LocalDateTime futureEndDate = DateUtil.safeCreateFromMinValue(year + 1, quarterNum * Constants.TrimesterMonthCount, 1).plusMonths(1); + ret.setFutureValue(new Pair<>(futureBeginDate, futureEndDate)); + } else if (endDate.compareTo(referenceDate) > 0) { + ret.setFutureValue(new Pair<>(beginDate, endDate)); + + LocalDateTime pastBeginDate = DateUtil.safeCreateFromMinValue(year - 1, (quarterNum - 1) * Constants.TrimesterMonthCount + 1, 1); + LocalDateTime pastEndDate = DateUtil.safeCreateFromMinValue(year - 1, quarterNum * Constants.TrimesterMonthCount, 1).plusMonths(1); + ret.setPastValue(new Pair<>(pastBeginDate, pastEndDate)); + } else { + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + } + + ret.setTimex(String.format("(%s,%s,P3M)", DateTimeFormatUtil.luisDate(-1, beginDate.getMonthValue(), 1), DateTimeFormatUtil.luisDate(-1, endDate.getMonthValue(), 1))); + } else { + ret.setFutureValue(new Pair<>(beginDate, endDate)); + ret.setPastValue(new Pair<>(beginDate, endDate)); + ret.setTimex(String.format("(%s,%s,P3M)", DateTimeFormatUtil.luisDate(beginDate), DateTimeFormatUtil.luisDate(endDate))); + } + + ret.setSuccess(true); + + return ret; + } + + private DateTimeResolutionResult parseSeason(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getSeasonRegex(), text, true); + if (match.getSuccess()) { + String seasonStr = this.config.getSeasonMap().get(match.getMatch().get().getGroup("seas").value.toLowerCase()); + + if (!match.getMatch().get().getGroup("EarlyPrefix").value.equals("")) { + ret.setMod(Constants.EARLY_MOD); + } else if (!match.getMatch().get().getGroup("MidPrefix").value.equals("")) { + ret.setMod(Constants.MID_MOD); + } else if (!match.getMatch().get().getGroup("LatePrefix").value.equals("")) { + ret.setMod(Constants.LATE_MOD); + } + + int year = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(match.getMatch().get()); + if (year == Constants.InvalidYear) { + int swift = this.config.getSwiftYear(text); + if (swift < -1) { + ret.setTimex(seasonStr); + ret.setSuccess(true); + return ret; + } + year = referenceDate.getYear() + swift; + } + + String yearStr = String.format("%04d", year); + ret.setTimex(String.format("%s-%s", yearStr, seasonStr)); + + ret.setSuccess(true); + return ret; + } + return ret; + } + + private DateTimeResolutionResult parseWeekOfDate(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getWeekOfRegex(), text)).findFirst(); + List dateErs = config.getDateExtractor().extract(text, referenceDate); + + if (dateErs.isEmpty()) { + // For cases like "week of the 18th" + dateErs.addAll( + config.getCardinalExtractor().extract(text).stream() + .peek(o -> o.setType(Constants.SYS_DATETIME_DATE)) + .filter(o -> dateErs.stream().noneMatch(er -> er.isOverlap(o))) + .collect(Collectors.toList())); + } + + if (match.isPresent() && dateErs.size() == 1) { + DateTimeResolutionResult pr = (DateTimeResolutionResult)config.getDateParser().parse(dateErs.get(0), referenceDate).getValue(); + if (config.getOptions().match(DateTimeOptions.CalendarMode)) { + LocalDateTime monday = DateUtil.thisDate((LocalDateTime)pr.getFutureValue(), DayOfWeek.MONDAY.getValue()); + ret.setTimex(DateTimeFormatUtil.toIsoWeekTimex(monday)); + } else { + ret.setTimex(pr.getTimex()); + } + ret.setComment(Constants.Comment_WeekOf); + ret.setFutureValue(getWeekRangeFromDate((LocalDateTime)pr.getFutureValue())); + ret.setPastValue(getWeekRangeFromDate((LocalDateTime)pr.getPastValue())); + ret.setSuccess(true); + } + return ret; + } + + private DateTimeResolutionResult parseMonthOfDate(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getMonthOfRegex(), text)).findFirst(); + List ex = config.getDateExtractor().extract(text, referenceDate); + + if (match.isPresent() && ex.size() == 1) { + DateTimeResolutionResult pr = (DateTimeResolutionResult)config.getDateParser().parse(ex.get(0), referenceDate).getValue(); + ret.setTimex(pr.getTimex()); + ret.setComment(Constants.Comment_MonthOf); + ret.setFutureValue(getMonthRangeFromDate((LocalDateTime)pr.getFutureValue())); + ret.setPastValue(getMonthRangeFromDate((LocalDateTime)pr.getPastValue())); + ret.setSuccess(true); + } + return ret; + } + + private Pair getWeekRangeFromDate(LocalDateTime date) { + LocalDateTime startDate = DateUtil.thisDate(date, DayOfWeek.MONDAY.getValue()); + LocalDateTime endDate = inclusiveEndPeriod ? startDate.plusDays(Constants.WeekDayCount - 1) : startDate.plusDays(Constants.WeekDayCount); + return new Pair<>(startDate, endDate); + } + + private Pair getMonthRangeFromDate(LocalDateTime date) { + LocalDateTime startDate = DateUtil.safeCreateFromMinValue(date.getYear(), date.getMonthValue(), 1); + LocalDateTime endDate; + + if (date.getMonthValue() < 12) { + endDate = DateUtil.safeCreateFromMinValue(date.getYear(), date.getMonthValue() + 1, 1); + } else { + endDate = DateUtil.safeCreateFromMinValue(date.getYear() + 1, 1, 1); + } + + endDate = inclusiveEndPeriod ? endDate.minusDays(1) : endDate; + return new Pair<>(startDate, endDate); + } + + private DateTimeResolutionResult parseWhichWeek(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getWhichWeekRegex(), text, true); + if (match.getSuccess()) { + int num = Integer.parseInt(match.getMatch().get().getGroup("number").value); + int year = referenceDate.getYear(); + ret.setTimex(String.format("%04d-W%02d", year, num)); + LocalDateTime firstDay = DateUtil.safeCreateFromMinValue(year, 1, 1); + LocalDateTime firstThursday = DateUtil.thisDate(firstDay, DayOfWeek.of(4).getValue()); + + if (DateUtil.weekOfYear(firstThursday) == 1) { + num -= 1; + } + + LocalDateTime value = firstThursday.plusDays(Constants.WeekDayCount * num - 3); + ret.setFutureValue(new Pair<>(value, value.plusDays(7))); + ret.setPastValue(new Pair<>(value, value.plusDays(7))); + ret.setSuccess(true); + } + return ret; + } + + private DateTimeResolutionResult getWeekOfMonth(String cardinalStr, int month, int year, LocalDateTime referenceDate, boolean noYear) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + LocalDateTime targetMonday = getMondayOfTargetWeek(cardinalStr, month, year); + + LocalDateTime futureDate = targetMonday; + LocalDateTime pastDate = targetMonday; + + if (noYear && futureDate.isBefore(referenceDate)) { + futureDate = getMondayOfTargetWeek(cardinalStr, month, year + 1); + } + + if (noYear && pastDate.compareTo(referenceDate) >= 0) { + pastDate = getMondayOfTargetWeek(cardinalStr, month, year - 1); + } + + if (noYear) { + year = Constants.InvalidYear; + } + + // Note that if the cardinalStr equals to "last", the weekNumber would be fixed at "5" + // This may lead to some inconsistency between Timex and Resolution + // the StartDate and EndDate of the resolution would always be correct (following ISO week definition) + // But week number for "last week" might be inconsistency with the resolution as we only have one Timex, + // but we may have past and future resolution which may have different week number + int weekNum = getWeekNumberForMonth(cardinalStr); + + String timex = TimexUtility.generateWeekOfMonthTimex(year, month, weekNum); + ret.setTimex(timex); + + ret.setFutureValue(inclusiveEndPeriod ? + new Pair<>(futureDate, futureDate.plusDays(Constants.WeekDayCount - 1)) : + new Pair<>(futureDate, futureDate.plusDays(Constants.WeekDayCount))); + ret.setPastValue(inclusiveEndPeriod ? + new Pair<>(pastDate, pastDate.plusDays(Constants.WeekDayCount - 1)) : + new Pair<>(pastDate, pastDate.plusDays(Constants.WeekDayCount))); + + ret.setSuccess(true); + + return ret; + } + + private LocalDateTime getFirstThursday(int year) { + return getFirstThursday(year, Constants.InvalidMonth); + } + + private LocalDateTime getFirstThursday(int year, int month) { + int targetMonth = month; + + if (month == Constants.InvalidMonth) { + targetMonth = Month.JANUARY.getValue(); + } + + LocalDateTime firstDay = LocalDateTime.of(year, targetMonth, 1, 0, 0); + LocalDateTime firstThursday = DateUtil.thisDate(firstDay, DayOfWeek.THURSDAY.getValue()); + + // Thursday fall into next year or next month + if (firstThursday.getMonthValue() != targetMonth) { + firstThursday = firstThursday.plusDays(Constants.WeekDayCount); + } + + return firstThursday; + } + + private LocalDateTime getLastThursday(int year) { + return getLastThursday(year, Constants.InvalidMonth); + } + + private LocalDateTime getLastThursday(int year, int month) { + int targetMonth = month; + + if (month == Constants.InvalidMonth) { + targetMonth = Month.DECEMBER.getValue(); + } + + LocalDateTime lastDay = getLastDay(year, targetMonth); + LocalDateTime lastThursday = DateUtil.thisDate(lastDay, DayOfWeek.THURSDAY.getValue()); + + // Thursday fall into next year or next month + if (lastThursday.getMonthValue() != targetMonth) { + lastThursday = lastThursday.minusDays(Constants.WeekDayCount); + } + + return lastThursday; + } + + private LocalDateTime getLastDay(int year, int month) { + month++; + if (month == 13) { + year++; + month = 1; + } + + LocalDateTime firstDayOfNextMonth = LocalDateTime.of(year, month, 1, 0, 0); + return firstDayOfNextMonth.minusDays(1); + } + + private LocalDateTime getMondayOfTargetWeek(String cardinalStr, int month, int year) { + LocalDateTime result; + if (config.isLastCardinal(cardinalStr)) { + LocalDateTime lastThursday = getLastThursday(year, month); + result = DateUtil.thisDate(lastThursday, DayOfWeek.MONDAY.getValue()); + } else { + int cardinal = getWeekNumberForMonth(cardinalStr); + LocalDateTime firstThursday = getFirstThursday(year, month); + + result = DateUtil.thisDate(firstThursday, DayOfWeek.MONDAY.getValue()).plusDays(Constants.WeekDayCount * (cardinal - 1)); + } + + return result; + } + + private int getWeekNumberForMonth(String cardinalStr) { + int cardinal; + + if (config.isLastCardinal(cardinalStr)) { + // "last week of month" might not be "5th week of month" + // Sometimes it can also be "4th week of month" depends on specific year and month + // But as we only have one Timex, so we use "5" to indicate last week of month + cardinal = Constants.MaxWeekOfMonth; + } else { + cardinal = config.getCardinalMap().get(cardinalStr); + } + + return cardinal; + } + + private DateTimeResolutionResult parseDecade(String text, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + int firstTwoNumOfYear = referenceDate.getYear() / 100; + int decade = 0; + int decadeLastYear = 10; + int swift = 1; + boolean inputCentury = false; + + String trimmedText = text.trim(); + ConditionalMatch match = RegexExtension.matchExact(this.config.getDecadeWithCenturyRegex(), text, true); + String beginLuisStr; + String endLuisStr; + + if (match.getSuccess()) { + + String decadeStr = match.getMatch().get().getGroup("decade").value.toLowerCase(); + if (!IntegerUtility.canParse(decadeStr)) { + if (this.config.getWrittenDecades().containsKey(decadeStr)) { + decade = this.config.getWrittenDecades().get(decadeStr); + } else if (this.config.getSpecialDecadeCases().containsKey(decadeStr)) { + firstTwoNumOfYear = this.config.getSpecialDecadeCases().get(decadeStr) / 100; + decade = this.config.getSpecialDecadeCases().get(decadeStr) % 100; + inputCentury = true; + } + } else { + decade = Integer.parseInt(decadeStr); + } + + String centuryStr = match.getMatch().get().getGroup("century").value.toLowerCase(); + if (!StringUtility.isNullOrEmpty(centuryStr)) { + if (!IntegerUtility.canParse(centuryStr)) { + if (this.config.getNumbers().containsKey(centuryStr)) { + firstTwoNumOfYear = this.config.getNumbers().get(centuryStr); + } else { + // handle the case like "one/two thousand", "one/two hundred", etc. + List er = this.config.getIntegerExtractor().extract(centuryStr); + + if (er.size() == 0) { + return ret; + } + + firstTwoNumOfYear = Math.round(((Double)(this.config.getNumberParser().parse(er.get(0)).getValue() != null ? + this.config.getNumberParser().parse(er.get(0)).getValue() : + 0)).floatValue()); + if (firstTwoNumOfYear >= 100) { + firstTwoNumOfYear = firstTwoNumOfYear / 100; + } + } + } else { + firstTwoNumOfYear = Integer.parseInt(centuryStr); + } + + inputCentury = true; + } + } else { + // handle cases like "the last 2 decades" "the next decade" + match = RegexExtension.matchExact(this.config.getRelativeDecadeRegex(), trimmedText, true); + if (match.getSuccess()) { + inputCentury = true; + + swift = this.config.getSwiftDayOrMonth(trimmedText); + + String numStr = match.getMatch().get().getGroup("number").value.toLowerCase(); + List er = this.config.getIntegerExtractor().extract(numStr); + if (er.size() == 1) { + int swiftNum = Math.round(((Double)(this.config.getNumberParser().parse(er.get(0)).getValue() != null ? + this.config.getNumberParser().parse(er.get(0)).getValue() : + 0)).floatValue()); + swift = swift * swiftNum; + } + + int beginDecade = (referenceDate.getYear() % 100) / 10; + if (swift < 0) { + beginDecade += swift; + } else if (swift > 0) { + beginDecade += 1; + } + + decade = beginDecade * 10; + } else { + return ret; + } + } + + int beginYear = firstTwoNumOfYear * 100 + decade; + // swift = 0 corresponding to the/this decade + int totalLastYear = decadeLastYear * Math.abs(swift == 0 ? 1 : swift); + + if (inputCentury) { + beginLuisStr = DateTimeFormatUtil.luisDate(beginYear, 1, 1); + endLuisStr = DateTimeFormatUtil.luisDate(beginYear + totalLastYear, 1, 1); + } else { + String beginYearStr = String.format("XX%s", decade); + beginLuisStr = DateTimeFormatUtil.luisDate(-1, 1, 1); + beginLuisStr = beginLuisStr.replace("XXXX", beginYearStr); + + String endYearStr = String.format("XX%s", (decade + totalLastYear)); + endLuisStr = DateTimeFormatUtil.luisDate(-1, 1, 1); + endLuisStr = endLuisStr.replace("XXXX", endYearStr); + } + ret.setTimex(String.format("(%s,%s,P%sY)", beginLuisStr, endLuisStr, totalLastYear)); + + int futureYear = beginYear; + int pastYear = beginYear; + LocalDateTime startDate = DateUtil.safeCreateFromMinValue(beginYear, 1, 1); + if (!inputCentury && startDate.isBefore(referenceDate)) { + futureYear += 100; + } + + if (!inputCentury && startDate.compareTo(referenceDate) >= 0) { + pastYear -= 100; + } + + ret.setFutureValue(new Pair<>(DateUtil.safeCreateFromMinValue(futureYear, 1, 1), + DateUtil.safeCreateFromMinValue(futureYear + totalLastYear, 1, 1))); + + ret.setPastValue(new Pair<>(DateUtil.safeCreateFromMinValue(pastYear, 1, 1), + DateUtil.safeCreateFromMinValue(pastYear + totalLastYear, 1, 1))); + + ret.setSuccess(true); + + return ret; + } + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } + + private DateContext getYearContext(String startDateStr, String endDateStr, String text) { + boolean isEndDatePureYear = false; + boolean isDateRelative = false; + int contextYear = Constants.InvalidYear; + + Optional yearMatchForEndDate = Arrays.stream(RegExpUtility.getMatches(this.config.getYearRegex(), endDateStr)).findFirst(); + + if (yearMatchForEndDate.isPresent() && yearMatchForEndDate.get().length == endDateStr.length()) { + isEndDatePureYear = true; + } + + Optional relativeMatchForStartDate = Arrays.stream(RegExpUtility.getMatches(this.config.getRelativeRegex(), startDateStr)).findFirst(); + Optional relativeMatchForEndDate = Arrays.stream(RegExpUtility.getMatches(this.config.getRelativeRegex(), endDateStr)).findFirst(); + isDateRelative = relativeMatchForStartDate.isPresent() || relativeMatchForEndDate.isPresent(); + + if (!isEndDatePureYear && !isDateRelative) { + for (Match match : RegExpUtility.getMatches(config.getYearRegex(), text)) { + int year = config.getDateExtractor().getYearFromText(match); + + if (year != Constants.InvalidYear) { + if (contextYear == Constants.InvalidYear) { + contextYear = year; + } else { + // This indicates that the text has two different year value, no common context year + if (contextYear != year) { + contextYear = Constants.InvalidYear; + break; + } + } + } + } + } + + DateContext dateContext = new DateContext(); + dateContext.setYear(contextYear); + return dateContext; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimeAltParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimeAltParser.java new file mode 100644 index 000000000..5eb012151 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimeAltParser.java @@ -0,0 +1,256 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtendedModelResult; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeAltParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import org.javatuples.Pair; + +public class BaseDateTimeAltParser implements IDateTimeParser { + + private static final String parserName = Constants.SYS_DATETIME_DATETIMEALT; + private final IDateTimeAltParserConfiguration config; + + public BaseDateTimeAltParser(IDateTimeAltParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return parserName; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + DateTimeResolutionResult value = null; + if (er.getType().equals(getParserName())) { + DateTimeResolutionResult innerResult = parseDateTimeAndTimeAlt(er, reference); + + if (innerResult.getSuccess()) { + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : value.getTimex()); + + return ret; + } + + // merge the entity with its related contexts and then parse the combine text + private DateTimeResolutionResult parseDateTimeAndTimeAlt(ExtractResult er, LocalDateTime referenceTime) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + // Original type of the extracted entity + String subType = ((Map)(er.getData())).get(Constants.SubType).toString(); + ExtractResult dateTimeEr = new ExtractResult(); + + // e.g. {next week Mon} or {Tue}, formmer--"next week Mon" doesn't contain "context" key + boolean hasContext = false; + ExtractResult contextEr = null; + if (((Map)er.getData()).containsKey(Constants.Context)) { + contextEr = (ExtractResult)((Map)er.getData()).get(Constants.Context); + if (contextEr.getType().equals(Constants.ContextType_RelativeSuffix)) { + dateTimeEr.setText(String.format("%s %s", er.getText(), contextEr.getText())); + } else { + dateTimeEr.setText(String.format("%s %s", contextEr.getText(), er.getText())); + } + + hasContext = true; + } else { + dateTimeEr.setText(er.getText()); + } + + dateTimeEr.setData(er.getData()); + DateTimeParseResult dateTimePr = null; + + if (subType.equals(Constants.SYS_DATETIME_DATE)) { + dateTimeEr.setType(Constants.SYS_DATETIME_DATE); + dateTimePr = this.config.getDateParser().parse(dateTimeEr, referenceTime); + } else if (subType.equals(Constants.SYS_DATETIME_TIME)) { + if (!hasContext) { + dateTimeEr.setType(Constants.SYS_DATETIME_TIME); + dateTimePr = this.config.getTimeParser().parse(dateTimeEr, referenceTime); + } else if (contextEr.getType().equals(Constants.SYS_DATETIME_DATE) || contextEr.getType().equals(Constants.ContextType_RelativePrefix)) { + // For cases: + // Monday 9 am or 11 am + // next 9 am or 11 am + dateTimeEr.setType(Constants.SYS_DATETIME_DATETIME); + dateTimePr = this.config.getDateTimeParser().parse(dateTimeEr, referenceTime); + } else if (contextEr.getType().equals(Constants.ContextType_AmPm)) { + // For cases: in the afternoon 3 o'clock or 5 o'clock + dateTimeEr.setType(Constants.SYS_DATETIME_TIME); + dateTimePr = this.config.getTimeParser().parse(dateTimeEr, referenceTime); + } + } else if (subType.equals(Constants.SYS_DATETIME_DATETIME)) { + // "next week Mon 9 am or Tue 1 pm" + dateTimeEr.setType(Constants.SYS_DATETIME_DATETIME); + dateTimePr = this.config.getDateTimeParser().parse(dateTimeEr, referenceTime); + } else if (subType.equals(Constants.SYS_DATETIME_TIMEPERIOD)) { + if (!hasContext) { + dateTimeEr.setType(Constants.SYS_DATETIME_TIMEPERIOD); + dateTimePr = this.config.getTimePeriodParser().parse(dateTimeEr, referenceTime); + } else if (contextEr.getType().equals(Constants.SYS_DATETIME_DATE) || contextEr.getType().equals(Constants.ContextType_RelativePrefix)) { + dateTimeEr.setType(Constants.SYS_DATETIME_DATETIMEPERIOD); + dateTimePr = this.config.getDateTimePeriodParser().parse(dateTimeEr, referenceTime); + } + } else if (subType.equals(Constants.SYS_DATETIME_DATETIMEPERIOD)) { + dateTimeEr.setType(Constants.SYS_DATETIME_DATETIMEPERIOD); + dateTimePr = this.config.getDateTimePeriodParser().parse(dateTimeEr, referenceTime); + } else if (subType.equals(Constants.SYS_DATETIME_DATEPERIOD)) { + dateTimeEr.setType(Constants.SYS_DATETIME_DATEPERIOD); + dateTimePr = this.config.getDatePeriodParser().parse(dateTimeEr, referenceTime); + } + + if (dateTimePr != null && dateTimePr.getValue() != null) { + ret.setFutureValue(((DateTimeResolutionResult)dateTimePr.getValue()).getFutureValue()); + ret.setPastValue(((DateTimeResolutionResult)dateTimePr.getValue()).getPastValue()); + ret.setTimex(dateTimePr.getTimexStr()); + + // Create resolution + getResolution(er, dateTimePr, ret); + + ret.setSuccess(true); + } + + return ret; + } + + private void getResolution(ExtractResult er, DateTimeParseResult pr, DateTimeResolutionResult ret) { + String parentText = ((Map)er.getData()).get(ExtendedModelResult.ParentTextKey).toString(); + String type = pr.getType(); + + boolean isPeriod = false; + boolean isSinglePoint = false; + String singlePointResolution = ""; + String pastStartPointResolution = ""; + String pastEndPointResolution = ""; + String futureStartPointResolution = ""; + String futureEndPointResolution = ""; + String singlePointType = ""; + String startPointType = ""; + String endPointType = ""; + + if (type.equals(Constants.SYS_DATETIME_DATEPERIOD) || type.equalsIgnoreCase(Constants.SYS_DATETIME_TIMEPERIOD) || + type.equals(Constants.SYS_DATETIME_DATETIMEPERIOD)) { + isPeriod = true; + switch (type) { + case Constants.SYS_DATETIME_DATEPERIOD: + startPointType = TimeTypeConstants.START_DATE; + endPointType = TimeTypeConstants.END_DATE; + pastStartPointResolution = DateTimeFormatUtil.formatDate(((Pair)ret.getPastValue()).getValue0()); + pastEndPointResolution = DateTimeFormatUtil.formatDate(((Pair)ret.getPastValue()).getValue1()); + futureStartPointResolution = DateTimeFormatUtil.formatDate(((Pair)ret.getFutureValue()).getValue0()); + futureEndPointResolution = DateTimeFormatUtil.formatDate(((Pair)ret.getFutureValue()).getValue1()); + break; + + case Constants.SYS_DATETIME_DATETIMEPERIOD: + startPointType = TimeTypeConstants.START_DATETIME; + endPointType = TimeTypeConstants.END_DATETIME; + + if (ret.getPastValue() instanceof Pair) { + pastStartPointResolution = DateTimeFormatUtil.formatDateTime(((Pair)ret.getPastValue()).getValue0()); + pastEndPointResolution = DateTimeFormatUtil.formatDateTime(((Pair)ret.getPastValue()).getValue1()); + futureStartPointResolution = DateTimeFormatUtil.formatDateTime(((Pair)ret.getFutureValue()).getValue0()); + futureEndPointResolution = DateTimeFormatUtil.formatDateTime(((Pair)ret.getFutureValue()).getValue1()); + } else if (ret.getPastValue() instanceof LocalDateTime) { + pastStartPointResolution = DateTimeFormatUtil.formatDateTime((LocalDateTime)ret.getPastValue()); + futureStartPointResolution = DateTimeFormatUtil.formatDateTime((LocalDateTime)ret.getFutureValue()); + } + + break; + + case Constants.SYS_DATETIME_TIMEPERIOD: + startPointType = TimeTypeConstants.START_TIME; + endPointType = TimeTypeConstants.END_TIME; + pastStartPointResolution = DateTimeFormatUtil.formatTime(((Pair)ret.getPastValue()).getValue0()); + pastEndPointResolution = DateTimeFormatUtil.formatTime(((Pair)ret.getPastValue()).getValue1()); + futureStartPointResolution = DateTimeFormatUtil.formatTime(((Pair)ret.getFutureValue()).getValue0()); + futureEndPointResolution = DateTimeFormatUtil.formatTime(((Pair)ret.getFutureValue()).getValue1()); + break; + default: + break; + } + } else { + isSinglePoint = true; + switch (type) { + case Constants.SYS_DATETIME_DATE: + singlePointType = TimeTypeConstants.DATE; + singlePointResolution = DateTimeFormatUtil.formatDate((LocalDateTime)ret.getFutureValue()); + break; + + case Constants.SYS_DATETIME_DATETIME: + singlePointType = TimeTypeConstants.DATETIME; + singlePointResolution = DateTimeFormatUtil.formatDateTime((LocalDateTime)ret.getFutureValue()); + break; + + case Constants.SYS_DATETIME_TIME: + singlePointType = TimeTypeConstants.TIME; + singlePointResolution = DateTimeFormatUtil.formatTime((LocalDateTime)ret.getFutureValue()); + break; + default: + break; + } + } + + if (isPeriod) { + ret.setFutureResolution(ImmutableMap.builder() + .put(startPointType, futureStartPointResolution) + .put(endPointType, futureEndPointResolution) + .put(ExtendedModelResult.ParentTextKey, parentText) + .build()); + + ret.setPastResolution(ImmutableMap.builder() + .put(startPointType, pastStartPointResolution) + .put(endPointType, pastEndPointResolution) + .put(ExtendedModelResult.ParentTextKey, parentText) + .build()); + } else if (isSinglePoint) { + ret.setFutureResolution(ImmutableMap.builder() + .put(singlePointType, singlePointResolution) + .put(ExtendedModelResult.ParentTextKey, parentText) + .build()); + + ret.setPastResolution(ImmutableMap.builder() + .put(singlePointType, singlePointResolution) + .put(ExtendedModelResult.ParentTextKey, parentText) + .build()); + } + + if (((DateTimeResolutionResult)pr.getValue()).getMod() != null) { + ret.setMod(((DateTimeResolutionResult)pr.getValue()).getMod()); + } + + if (((DateTimeResolutionResult)pr.getValue()).getTimeZoneResolution() != null) { + ret.setTimeZoneResolution(((DateTimeResolutionResult)pr.getValue()).getTimeZoneResolution()); + } + } + + @Override + public List filterResults(String query, List candidateResults) { + + return candidateResults; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimeParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimeParser.java new file mode 100644 index 000000000..835a21d7c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimeParser.java @@ -0,0 +1,398 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultTimex; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.AgoLaterUtil; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class BaseDateTimeParser implements IDateTimeParser { + + private final IDateTimeParserConfiguration config; + + public BaseDateTimeParser(IDateTimeParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_DATETIME; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + LocalDateTime referenceDate = reference; + + Object value = null; + + if (er.getType().equals(getParserName())) { + DateTimeResolutionResult innerResult = this.mergeDateAndTime(er.getText(), referenceDate); + + if (!innerResult.getSuccess()) { + innerResult = this.parseBasicRegex(er.getText(), referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseTimeOfToday(er.getText(), referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseSpecialTimeOfDate(er.getText(), referenceDate); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parserDurationWithAgoAndLater(er.getText(), referenceDate); + } + + if (innerResult.getSuccess()) { + Map futureResolution = ImmutableMap.builder() + .put(TimeTypeConstants.DATETIME, + DateTimeFormatUtil.formatDateTime((LocalDateTime)innerResult.getFutureValue())) + .build(); + innerResult.setFutureResolution(futureResolution); + + Map pastResolution = ImmutableMap.builder() + .put(TimeTypeConstants.DATETIME, + DateTimeFormatUtil.formatDateTime((LocalDateTime)innerResult.getPastValue())) + .build(); + innerResult.setPastResolution(pastResolution); + + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult(er.getStart(), er.getLength(), er.getText(), er.getType(), er.getData(), value, "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex()); + + return ret; + } + + @Override + public List filterResults(String query, List candidateResults) { + throw new UnsupportedOperationException(); + } + + // Merge a Date entity and a Time entity + private DateTimeResolutionResult mergeDateAndTime(String text, LocalDateTime reference) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + List ersDate = config.getDateExtractor().extract(text, reference); + if (ersDate.isEmpty()) { + ersDate = config.getDateExtractor().extract(config.getTokenBeforeDate() + text, reference); + if (ersDate.size() == 1) { + int newStart = ersDate.get(0).getStart() - config.getTokenBeforeDate().length(); + ersDate.get(0).setStart(newStart); + ersDate.set(0, ersDate.get(0)); + } else { + return result; + } + } else { + // This is to understand if there is an ambiguous token in the text. For some + // languages (e.g. spanish), + // the same word could mean different things (e.g a time in the day or an + // specific day). + if (config.containsAmbiguousToken(text, ersDate.get(0).getText())) { + return result; + } + } + + List ersTime = config.getTimeExtractor().extract(text, reference); + if (ersTime.isEmpty()) { + // Here we filter out "morning, afternoon, night..." time entities + ersTime = config.getTimeExtractor().extract(config.getTokenBeforeTime() + text, reference); + if (ersTime.size() == 1) { + int newStart = ersTime.get(0).getStart() - config.getTokenBeforeTime().length(); + ersTime.get(0).setStart(newStart); + ersTime.set(0, ersTime.get(0)); + } else if (ersTime.isEmpty()) { + // check whether there is a number being used as a time point + boolean hasTimeNumber = false; + List numErs = config.getIntegerExtractor().extract(text); + if (!numErs.isEmpty() && ersDate.size() == 1) { + for (ExtractResult num : numErs) { + int middleBegin = ersDate.get(0).getStart() + ersDate.get(0).getLength(); + int middleEnd = num.getStart(); + if (middleBegin > middleEnd) { + continue; + } + + String middleStr = text.substring(middleBegin, middleEnd).trim().toLowerCase(); + Optional match = Arrays + .stream(RegExpUtility.getMatches(config.getDateNumberConnectorRegex(), middleStr)) + .findFirst(); + if (StringUtility.isNullOrEmpty(middleStr) || match.isPresent()) { + num.setType(Constants.SYS_DATETIME_TIME); + ersTime.add(num); + hasTimeNumber = true; + } + } + } + + if (!hasTimeNumber) { + return result; + } + } + } + + // Handle cases like "Oct. 5 in the afternoon at 7:00"; + // in this case "5 in the afternoon" will be extracted as a Time entity + int correctTimeIdx = 0; + while (correctTimeIdx < ersTime.size() && ersTime.get(correctTimeIdx).isOverlap(ersDate.get(0))) { + correctTimeIdx++; + } + + if (correctTimeIdx >= ersTime.size()) { + return result; + } + + DateTimeParseResult prDate = config.getDateParser().parse(ersDate.get(0), reference); + DateTimeParseResult prTime = config.getTimeParser().parse(ersTime.get(correctTimeIdx), reference); + + if (prDate.getValue() == null || prTime.getValue() == null) { + return result; + } + + LocalDateTime futureDate = (LocalDateTime)((DateTimeResolutionResult)prDate.getValue()).getFutureValue(); + LocalDateTime pastDate = (LocalDateTime)((DateTimeResolutionResult)prDate.getValue()).getPastValue(); + LocalDateTime time = (LocalDateTime)((DateTimeResolutionResult)prTime.getValue()).getPastValue(); + + int hour = time.getHour(); + int min = time.getMinute(); + int sec = time.getSecond(); + + // Handle morning, afternoon + if (RegExpUtility.getMatches(config.getPMTimeRegex(), text).length != 0 && withinAfternoonHours(hour)) { + hour += Constants.HalfDayHourCount; + } else if (RegExpUtility.getMatches(config.getAMTimeRegex(), text).length != 0 && + withinMorningHoursAndNoon(hour, min, sec)) { + hour -= Constants.HalfDayHourCount; + } + + String timeStr = prTime.getTimexStr(); + if (timeStr.endsWith(Constants.Comment_AmPm)) { + timeStr = timeStr.substring(0, timeStr.length() - 4); + } + + timeStr = String.format("T%02d%s", hour, timeStr.substring(3)); + result.setTimex(prDate.getTimexStr() + timeStr); + DateTimeResolutionResult val = (DateTimeResolutionResult)prTime.getValue(); + if (hour <= Constants.HalfDayHourCount && RegExpUtility.getMatches(config.getPMTimeRegex(), text).length == 0 && + RegExpUtility.getMatches(config.getAMTimeRegex(), text).length == 0 && + !StringUtility.isNullOrEmpty(val.getComment())) { + result.setComment(Constants.Comment_AmPm); + } + + result.setFutureValue(DateUtil.safeCreateFromMinValue(futureDate.getYear(), futureDate.getMonthValue(), + futureDate.getDayOfMonth(), hour, min, sec)); + result.setPastValue(DateUtil.safeCreateFromMinValue(pastDate.getYear(), pastDate.getMonthValue(), + pastDate.getDayOfMonth(), hour, min, sec)); + + result.setSuccess(true); + + // Change the value of time object + prTime.setTimexStr(timeStr); + if (!StringUtility.isNullOrEmpty(result.getComment())) { + DateTimeResolutionResult newValue = (DateTimeResolutionResult)prTime.getValue(); + newValue.setComment(result.getComment().equals(Constants.Comment_AmPm) ? Constants.Comment_AmPm : ""); + prTime.setValue(newValue); + prTime.setTimexStr(timeStr); + } + + // Add the date and time object in case we want to split them + List entities = new ArrayList<>(); + entities.add(prDate); + entities.add(prTime); + result.setSubDateTimeEntities(entities); + + result.setTimeZoneResolution(((DateTimeResolutionResult)prTime.getValue()).getTimeZoneResolution()); + + return result; + } + + private DateTimeResolutionResult parseBasicRegex(String text, LocalDateTime reference) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + String trimmedText = text.trim().toLowerCase(); + + // Handle "now" + if (RegexExtension.isExactMatch(config.getNowRegex(), trimmedText, true)) { + ResultTimex timexResult = config.getMatchedNowTimex(trimmedText); + result.setTimex(timexResult.getTimex()); + result.setFutureValue(reference); + result.setPastValue(reference); + result.setSuccess(true); + } + + return result; + } + + private DateTimeResolutionResult parseTimeOfToday(String text, LocalDateTime reference) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + String trimmedText = text.trim().toLowerCase(); + + int hour = 0; + int minute = 0; + int second = 0; + String timeStr; + + ConditionalMatch wholeMatch = RegexExtension.matchExact(config.getSimpleTimeOfTodayAfterRegex(), trimmedText, true); + if (!wholeMatch.getSuccess()) { + wholeMatch = RegexExtension.matchExact(config.getSimpleTimeOfTodayBeforeRegex(), trimmedText, true); + } + + if (wholeMatch.getSuccess()) { + String hourStr = wholeMatch.getMatch().get().getGroup(Constants.HourGroupName).value; + if (StringUtility.isNullOrEmpty(hourStr)) { + hourStr = wholeMatch.getMatch().get().getGroup("hournum").value.toLowerCase(); + hour = config.getNumbers().get(hourStr); + } else { + hour = Integer.parseInt(hourStr); + } + + timeStr = String.format("T%02d", hour); + } else { + List ers = config.getTimeExtractor().extract(trimmedText, reference); + if (ers.size() != 1) { + ers = config.getTimeExtractor().extract(config.getTokenBeforeTime() + trimmedText, reference); + if (ers.size() == 1) { + int newStart = ers.get(0).getStart() - config.getTokenBeforeTime().length(); + ers.get(0).setStart(newStart); + ers.set(0, ers.get(0)); + } else { + return result; + } + } + + DateTimeParseResult pr = config.getTimeParser().parse(ers.get(0), reference); + if (pr.getValue() == null) { + return result; + } + + LocalDateTime time = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getFutureValue(); + hour = time.getHour(); + minute = time.getMinute(); + second = time.getSecond(); + timeStr = pr.getTimexStr(); + } + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getSpecificTimeOfDayRegex(), trimmedText)) + .findFirst(); + + if (match.isPresent()) { + String matchStr = match.get().value.toLowerCase(); + + // Handle "last", "next" + int swift = config.getSwiftDay(matchStr); + LocalDateTime date = reference.plusDays(swift); + + // Handle "morning", "afternoon" + hour = config.getHour(matchStr, hour); + + // In this situation, timeStr cannot end up with "ampm", because we always have + // a "morning" or "night" + if (timeStr.endsWith(Constants.Comment_AmPm)) { + timeStr = timeStr.substring(0, timeStr.length() - 4); + } + + timeStr = String.format("T%02d%s", hour, timeStr.substring(3)); + + result.setTimex(DateTimeFormatUtil.formatDate(date) + timeStr); + LocalDateTime dateResult = DateUtil.safeCreateFromMinValue(date.getYear(), date.getMonthValue(), + date.getDayOfMonth(), hour, minute, second); + + result.setFutureValue(dateResult); + result.setPastValue(dateResult); + result.setSuccess(true); + } + + return result; + } + + private DateTimeResolutionResult parseSpecialTimeOfDate(String text, LocalDateTime reference) { + DateTimeResolutionResult result = parseUnspecificTimeOfDate(text, reference); + + if (result.getSuccess()) { + return result; + } + + List ers = config.getDateExtractor().extract(text, reference); + if (ers.size() != 1) { + return result; + } + + String beforeStr = text.substring(0, ers.get(0).getStart()); + if (RegExpUtility.getMatches(config.getSpecificEndOfRegex(), beforeStr).length != 0) { + DateTimeParseResult pr = config.getDateParser().parse(ers.get(0), reference); + LocalDateTime futureDate = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getFutureValue(); + LocalDateTime pastDate = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getPastValue(); + + result = resolveEndOfDay(pr.getTimexStr(), futureDate, pastDate); + } + + return result; + } + + private DateTimeResolutionResult parseUnspecificTimeOfDate(String text, LocalDateTime reference) { + // Handle 'eod', 'end of day' + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + Optional eod = Arrays.stream(RegExpUtility.getMatches(config.getUnspecificEndOfRegex(), text)).findFirst(); + + if (eod.isPresent()) { + result = resolveEndOfDay(DateTimeFormatUtil.formatDate(reference), reference, reference); + } + + return result; + } + + private DateTimeResolutionResult resolveEndOfDay(String timexPrefix, LocalDateTime futureDate, LocalDateTime pastDate) { + String timex = String.format("%sT23:59:59", timexPrefix); + LocalDateTime futureValue = LocalDateTime.of(futureDate.toLocalDate(), LocalTime.MIDNIGHT).plusDays(1).minusSeconds(1); + LocalDateTime pastValue = LocalDateTime.of(pastDate.toLocalDate(), LocalTime.MIDNIGHT).plusDays(1).minusSeconds(1); + + DateTimeResolutionResult result = new DateTimeResolutionResult(); + result.setTimex(timex); + result.setFutureValue(futureValue); + result.setPastValue(pastValue); + result.setSuccess(true); + + return result; + } + + private boolean withinAfternoonHours(int hour) { + return hour < Constants.HalfDayHourCount; + } + + private boolean withinMorningHoursAndNoon(int hour, int min, int sec) { + return (hour > Constants.HalfDayHourCount || (hour == Constants.HalfDayHourCount && (min > 0 || sec > 0))); + } + + private DateTimeResolutionResult parserDurationWithAgoAndLater(String text, LocalDateTime reference) { + return AgoLaterUtil.parseDurationWithAgoAndLater(text, reference, config.getDurationExtractor(), + config.getDurationParser(), config.getUnitMap(), config.getUnitRegex(), + config.getUtilityConfiguration(), config::getSwiftDay); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimePeriodParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimePeriodParser.java new file mode 100644 index 000000000..011ceb157 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDateTimePeriodParser.java @@ -0,0 +1,1036 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RangeTimexComponents; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneUtility; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.javatuples.Pair; + +public class BaseDateTimePeriodParser implements IDateTimeParser { + + protected final IDateTimePeriodParserConfiguration config; + + public BaseDateTimePeriodParser(IDateTimePeriodParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_DATETIMEPERIOD; + } + + @Override + public List filterResults(String query, List candidateResults) { + throw new UnsupportedOperationException(); + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + LocalDateTime referenceDate = reference; + + Object value = null; + + if (er.getType().equals(getParserName())) { + DateTimeResolutionResult innerResult = internalParse(er.getText(), referenceDate); + + if (TimeZoneUtility.shouldResolveTimeZone(er, config.getOptions())) { + Map metadata = (HashMap)er.getData(); + + ExtractResult timezoneEr = (ExtractResult)metadata.get(Constants.SYS_DATETIME_TIMEZONE); + ParseResult timezonePr = config.getTimeZoneParser().parse(timezoneEr); + if (timezonePr.getValue() != null) { + innerResult.setTimeZoneResolution(((DateTimeResolutionResult)timezonePr.getValue()).getTimeZoneResolution()); + } + } + + if (innerResult.getSuccess()) { + if (!isBeforeOrAfterMod(innerResult.getMod())) { + Map futureResolution = ImmutableMap.builder() + .put(TimeTypeConstants.START_DATETIME, + DateTimeFormatUtil.formatDateTime(((Pair)innerResult.getFutureValue()).getValue0())) + .put(TimeTypeConstants.END_DATETIME, DateTimeFormatUtil.formatDateTime(((Pair)innerResult.getFutureValue()).getValue1())) + .build(); + innerResult.setFutureResolution(futureResolution); + + Map pastResolution = ImmutableMap.builder() + .put(TimeTypeConstants.START_DATETIME, DateTimeFormatUtil.formatDateTime(((Pair)innerResult.getPastValue()).getValue0())) + .put(TimeTypeConstants.END_DATETIME, DateTimeFormatUtil.formatDateTime(((Pair)innerResult.getPastValue()).getValue1())) + .build(); + innerResult.setPastResolution(pastResolution); + + } else { + if (innerResult.getMod().equals(Constants.AFTER_MOD)) { + // Cases like "1/1/2015 after 2:00" there is no EndTime + Map futureResolution = ImmutableMap.builder() + .put(TimeTypeConstants.START_DATETIME, DateTimeFormatUtil.formatDateTime((LocalDateTime)innerResult.getFutureValue())) + .build(); + innerResult.setFutureResolution(futureResolution); + + Map pastResolution = ImmutableMap.builder() + .put(TimeTypeConstants.START_DATETIME, DateTimeFormatUtil.formatDateTime((LocalDateTime)innerResult.getPastValue())) + .build(); + innerResult.setPastResolution(pastResolution); + } else { + // Cases like "1/1/2015 before 5:00 in the afternoon" there is no StartTime + Map futureResolution = ImmutableMap.builder() + .put(TimeTypeConstants.END_DATETIME, DateTimeFormatUtil.formatDateTime((LocalDateTime)innerResult.getFutureValue())) + .build(); + innerResult.setFutureResolution(futureResolution); + + Map pastResolution = ImmutableMap.builder() + .put(TimeTypeConstants.END_DATETIME, DateTimeFormatUtil.formatDateTime((LocalDateTime)innerResult.getPastValue())) + .build(); + innerResult.setPastResolution(pastResolution); + } + } + + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex()); + + return ret; + } + + private DateTimeResolutionResult internalParse(String text, LocalDateTime reference) { + DateTimeResolutionResult innerResult = this.mergeDateAndTimePeriods(text, reference); + + if (!innerResult.getSuccess()) { + innerResult = this.mergeTwoTimePoints(text, reference); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseSpecificTimeOfDay(text, reference); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseDuration(text, reference); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseRelativeUnit(text, reference); + } + + if (!innerResult.getSuccess()) { + innerResult = this.parseDateWithPeriodPrefix(text, reference); + } + + if (!innerResult.getSuccess()) { + // Cases like "today after 2:00pm", "1/1/2015 before 2:00 in the afternoon" + innerResult = this.parseDateWithTimePeriodSuffix(text, reference); + } + + return innerResult; + } + + private boolean isBeforeOrAfterMod(String mod) { + return !StringUtility.isNullOrEmpty(mod) && + (mod == Constants.BEFORE_MOD || mod == Constants.AFTER_MOD); + } + + private DateTimeResolutionResult mergeDateAndTimePeriods(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + String trimmedText = text.trim().toLowerCase(); + + List ers = config.getTimePeriodExtractor().extract(trimmedText, referenceTime); + + if (ers.size() == 0) { + return parsePureNumberCases(text, referenceTime); + } else if (ers.size() == 1) { + ParseResult timePeriodParseResult = config.getTimePeriodParser().parse(ers.get(0)); + DateTimeResolutionResult timePeriodResolutionResult = (DateTimeResolutionResult)timePeriodParseResult.getValue(); + + if (timePeriodResolutionResult == null) { + return parsePureNumberCases(text, referenceTime); + } + + String timePeriodTimex = timePeriodResolutionResult.getTimex(); + + + // If it is a range type timex + if (TimexUtility.isRangeTimex(timePeriodTimex)) { + List dateResult = config.getDateExtractor().extract(trimmedText.replace(ers.get(0).getText(), ""), referenceTime); + String dateText = trimmedText.replace(ers.get(0).getText(), "").replace(config.getTokenBeforeDate(), "").trim(); + + // If only one Date is extracted and the Date text equals to the rest part of source text + if (dateResult.size() == 1 && dateText.equals(dateResult.get(0).getText())) { + String dateTimex; + LocalDateTime futureTime; + LocalDateTime pastTime; + + DateTimeParseResult pr = config.getDateParser().parse(dateResult.get(0), referenceTime); + + if (pr.getValue() != null) { + futureTime = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getFutureValue(); + pastTime = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getPastValue(); + + dateTimex = pr.getTimexStr(); + } else { + return parsePureNumberCases(text, referenceTime); + } + + RangeTimexComponents rangeTimexComponents = TimexUtility.getRangeTimexComponents(timePeriodTimex); + if (rangeTimexComponents.isValid) { + String beginTimex = TimexUtility.combineDateAndTimeTimex(dateTimex, rangeTimexComponents.beginTimex); + String endTimex = TimexUtility.combineDateAndTimeTimex(dateTimex, rangeTimexComponents.endTimex); + ret.setTimex(TimexUtility.generateDateTimePeriodTimex(beginTimex, endTimex, rangeTimexComponents.durationTimex)); + + Pair timePeriodFutureValue = (Pair)timePeriodResolutionResult.getFutureValue(); + LocalDateTime beginTime = timePeriodFutureValue.getValue0(); + LocalDateTime endTime = timePeriodFutureValue.getValue1(); + + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromMinValue(futureTime.getYear(), futureTime.getMonthValue(), futureTime.getDayOfMonth(), + beginTime.getHour(), beginTime.getMinute(), beginTime.getSecond()), + DateUtil.safeCreateFromMinValue(futureTime.getYear(), futureTime.getMonthValue(), futureTime.getDayOfMonth(), + endTime.getHour(), endTime.getMinute(), endTime.getSecond()) + )); + + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromMinValue(pastTime.getYear(), pastTime.getMonthValue(), pastTime.getDayOfMonth(), + beginTime.getHour(), beginTime.getMinute(), beginTime.getSecond()), + DateUtil.safeCreateFromMinValue(pastTime.getYear(), pastTime.getMonthValue(), pastTime.getDayOfMonth(), + endTime.getHour(), endTime.getMinute(), endTime.getSecond()) + )); + + + if (!StringUtility.isNullOrEmpty(timePeriodResolutionResult.getComment()) && + timePeriodResolutionResult.getComment().equals(Constants.Comment_AmPm)) { + // AmPm comment is used for later SetParserResult to judge whether this parse comments should have two parsing results + // Cases like "from 10:30 to 11 on 1/1/2015" should have AmPm comment, as it can be parsed to "10:30am to 11am" and also be parsed to "10:30pm to 11pm" + // Cases like "from 10:30 to 3 on 1/1/2015" should not have AmPm comment + if (beginTime.getHour() < Constants.HalfDayHourCount && endTime.getHour() < Constants.HalfDayHourCount) { + ret.setComment(Constants.Comment_AmPm); + } + } + + ret.setSuccess(true); + List subDateTimeEntities = new ArrayList<>(); + subDateTimeEntities.add(pr); + subDateTimeEntities.add(timePeriodParseResult); + ret.setSubDateTimeEntities(subDateTimeEntities); + + return ret; + } + } + + return parsePureNumberCases(text, referenceTime); + } + } + + return ret; + } + + // Handle cases like "Monday 7-9", where "7-9" can't be extracted by the TimePeriodExtractor + private DateTimeResolutionResult parsePureNumberCases(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + String trimmedText = text.trim().toLowerCase(); + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getPureNumberFromToRegex(), trimmedText)).findFirst(); + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getPureNumberBetweenAndRegex(), trimmedText)).findFirst(); + } + + if (match.isPresent() && (match.get().index == 0 || match.get().index + match.get().length == trimmedText.length())) { + ParseTimePeriodResult parseTimePeriodResult = parseTimePeriod(match.get()); + int beginHour = parseTimePeriodResult.beginHour; + int endHour = parseTimePeriodResult.endHour; + ret.setComment(parseTimePeriodResult.comments); + + String dateStr = ""; + + // Parse following date + List ers = config.getDateExtractor().extract(trimmedText.replace(match.get().value, ""), referenceTime); + LocalDateTime futureDate; + LocalDateTime pastDate; + + if (ers.size() > 0) { + DateTimeParseResult pr = config.getDateParser().parse(ers.get(0), referenceTime); + if (pr.getValue() != null) { + DateTimeResolutionResult prValue = (DateTimeResolutionResult)pr.getValue(); + futureDate = (LocalDateTime)prValue.getFutureValue(); + pastDate = (LocalDateTime)prValue.getPastValue(); + + dateStr = pr.getTimexStr(); + + } else { + return ret; + } + } else { + return ret; + } + + int pastHours = endHour - beginHour; + String beginTimex = TimexUtility.combineDateAndTimeTimex(dateStr, DateTimeFormatUtil.shortTime(beginHour)); + String endTimex = TimexUtility.combineDateAndTimeTimex(dateStr, DateTimeFormatUtil.shortTime(endHour)); + String durationTimex = TimexUtility.generateDurationTimex(endHour - beginHour, Constants.TimexHour, true); + + ret.setTimex(TimexUtility.generateDateTimePeriodTimex(beginTimex, endTimex, durationTimex)); + + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromMinValue(futureDate.getYear(), futureDate.getMonthValue(), futureDate.getDayOfMonth(), + beginHour, 0, 0), + DateUtil.safeCreateFromMinValue(futureDate.getYear(), futureDate.getMonthValue(), futureDate.getDayOfMonth(), + endHour, 0, 0) + )); + + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromMinValue(pastDate.getYear(), pastDate.getMonthValue(), pastDate.getDayOfMonth(), + beginHour, 0, 0), + DateUtil.safeCreateFromMinValue(pastDate.getYear(), pastDate.getMonthValue(), pastDate.getDayOfMonth(), + endHour, 0, 0) + )); + + ret.setSuccess(true); + } + + return ret; + } + + private ParseTimePeriodResult parseTimePeriod(Match match) { + + ParseTimePeriodResult result = new ParseTimePeriodResult(); + + // This "from .. to .." pattern is valid if followed by a Date OR "pm" + boolean hasAm = false; + boolean hasPm = false; + String comments = ""; + + // Get hours + MatchGroup hourGroup = match.getGroup(Constants.HourGroupName); + String hourStr = hourGroup.captures[0].value; + + if (this.config.getNumbers().containsKey(hourStr)) { + result.beginHour = this.config.getNumbers().get(hourStr); + } else { + result.beginHour = Integer.parseInt(hourStr); + } + + hourStr = hourGroup.captures[1].value; + + if (this.config.getNumbers().containsKey(hourStr)) { + result.endHour = this.config.getNumbers().get(hourStr); + } else { + result.endHour = Integer.parseInt(hourStr); + } + + // Parse "pm" + String pmStr = match.getGroup(Constants.PmGroupName).value; + String amStr = match.getGroup(Constants.AmGroupName).value; + String descStr = match.getGroup(Constants.DescGroupName).value; + if (!StringUtility.isNullOrEmpty(amStr) || !StringUtility.isNullOrEmpty(descStr) && descStr.startsWith("a")) { + if (result.beginHour >= Constants.HalfDayHourCount) { + result.beginHour -= Constants.HalfDayHourCount; + } + + if (result.endHour >= Constants.HalfDayHourCount) { + result.endHour -= Constants.HalfDayHourCount; + } + + hasAm = true; + } else if (!StringUtility.isNullOrEmpty(pmStr) || !StringUtility.isNullOrEmpty(descStr) && descStr.startsWith("p")) { + if (result.beginHour < Constants.HalfDayHourCount) { + result.beginHour += Constants.HalfDayHourCount; + } + + if (result.endHour < Constants.HalfDayHourCount) { + result.endHour += Constants.HalfDayHourCount; + } + + hasPm = true; + } + + if (!hasAm && !hasPm && result.beginHour <= Constants.HalfDayHourCount && result.endHour <= Constants.HalfDayHourCount) { + if (result.beginHour > result.endHour) { + if (result.beginHour == Constants.HalfDayHourCount) { + result.beginHour = 0; + } else { + result.endHour += Constants.HalfDayHourCount; + } + } + + result.comments = Constants.Comment_AmPm; + } + + return result; + } + + private DateTimeResolutionResult mergeTwoTimePoints(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + DateTimeParseResult pr1; + DateTimeParseResult pr2; + boolean bothHaveDates = false; + boolean beginHasDate = false; + boolean endHasDate = false; + + List timeExtractResults = config.getTimeExtractor().extract(text, referenceDate); + List dateTimeExtractResults = config.getDateTimeExtractor().extract(text, referenceDate); + + if (dateTimeExtractResults.size() == 2) { + pr1 = config.getDateTimeParser().parse(dateTimeExtractResults.get(0), referenceDate); + pr2 = config.getDateTimeParser().parse(dateTimeExtractResults.get(1), referenceDate); + bothHaveDates = true; + } else if (dateTimeExtractResults.size() == 1 && timeExtractResults.size() == 2) { + if (!dateTimeExtractResults.get(0).isOverlap(timeExtractResults.get(0))) { + pr1 = config.getTimeParser().parse(timeExtractResults.get(0), referenceDate); + pr2 = config.getDateTimeParser().parse(dateTimeExtractResults.get(0), referenceDate); + endHasDate = true; + } else { + pr1 = config.getDateTimeParser().parse(dateTimeExtractResults.get(0), referenceDate); + pr2 = config.getTimeParser().parse(timeExtractResults.get(1), referenceDate); + beginHasDate = true; + } + } else if (dateTimeExtractResults.size() == 1 && timeExtractResults.size() == 1) { + if (timeExtractResults.get(0).getStart() < dateTimeExtractResults.get(0).getStart()) { + pr1 = config.getTimeParser().parse(timeExtractResults.get(0), referenceDate); + pr2 = config.getDateTimeParser().parse(dateTimeExtractResults.get(0), referenceDate); + endHasDate = true; + } else if (timeExtractResults.get(0).getStart() >= dateTimeExtractResults.get(0).getStart() + dateTimeExtractResults.get(0).getLength()) { + pr1 = config.getDateTimeParser().parse(dateTimeExtractResults.get(0), referenceDate); + pr2 = config.getTimeParser().parse(timeExtractResults.get(0), referenceDate); + beginHasDate = true; + } else { + // If the only TimeExtractResult is part of DateTimeExtractResult, then it should not be handled in this method + return result; + } + } else if (timeExtractResults.size() == 2) { + // If both ends are Time. then this is a TimePeriod, not a DateTimePeriod + return result; + } else { + return result; + } + + if (pr1.getValue() == null || pr2.getValue() == null) { + return result; + } + + LocalDateTime futureBegin = (LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getFutureValue(); + LocalDateTime futureEnd = (LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getFutureValue(); + + LocalDateTime pastBegin = (LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getPastValue(); + LocalDateTime pastEnd = (LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getPastValue(); + + if (bothHaveDates) { + if (futureBegin.isAfter(futureEnd)) { + futureBegin = pastBegin; + } + + if (pastEnd.isBefore(pastBegin)) { + pastEnd = futureEnd; + } + } + + if (bothHaveDates) { + result.setTimex(String.format("(%s,%s,PT%dH)", pr1.getTimexStr(), pr2.getTimexStr(), Math.round(ChronoUnit.SECONDS.between(futureBegin, futureEnd) / 3600f))); + // Do nothing + } else if (beginHasDate) { + futureEnd = DateUtil.safeCreateFromMinValue(futureBegin.toLocalDate(), futureEnd.toLocalTime()); + pastEnd = DateUtil.safeCreateFromMinValue(pastBegin.toLocalDate(), pastEnd.toLocalTime()); + + String dateStr = pr1.getTimexStr().split("T")[0]; + result.setTimex(String.format("(%s,%s,PT%dH)", pr1.getTimexStr(), dateStr + pr2.getTimexStr(), ChronoUnit.HOURS.between(futureBegin, futureEnd))); + } else if (endHasDate) { + futureBegin = DateUtil.safeCreateFromMinValue(futureEnd.getYear(), futureEnd.getMonthValue(), futureEnd.getDayOfMonth(), + futureBegin.getHour(), futureBegin.getMinute(), futureBegin.getSecond()); + + pastBegin = DateUtil.safeCreateFromMinValue(pastEnd.getYear(), pastEnd.getMonthValue(), pastEnd.getDayOfMonth(), + pastBegin.getHour(), pastBegin.getMinute(), pastBegin.getSecond()); + + + String dateStr = pr2.getTimexStr().split("T")[0]; + result.setTimex(String.format("(%s,%s,PT%dH)", dateStr + pr1.getTimexStr(), pr2.getTimexStr(), ChronoUnit.HOURS.between(futureBegin, futureEnd))); + } + + DateTimeResolutionResult pr1Value = (DateTimeResolutionResult)pr1.getValue(); + DateTimeResolutionResult pr2Value = (DateTimeResolutionResult)pr2.getValue(); + + String ampmStr1 = pr1Value.getComment(); + String ampmStr2 = pr2Value.getComment(); + + if (!StringUtility.isNullOrEmpty(ampmStr1) && ampmStr1.endsWith(Constants.Comment_AmPm) && + !StringUtility.isNullOrEmpty(ampmStr2) && ampmStr2.endsWith(Constants.Comment_AmPm)) { + result.setComment(Constants.Comment_AmPm); + } + + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + if (pr1Value.getTimeZoneResolution() != null) { + result.setTimeZoneResolution(pr1Value.getTimeZoneResolution()); + } + + if (pr2Value.getTimeZoneResolution() != null) { + result.setTimeZoneResolution(pr2Value.getTimeZoneResolution()); + } + } + + result.setFutureValue(new Pair(futureBegin, futureEnd)); + result.setPastValue(new Pair(pastBegin, pastEnd)); + + result.setSuccess(true); + + List subDateTimeEntities = new ArrayList<>(); + subDateTimeEntities.add(pr1); + subDateTimeEntities.add(pr2); + result.setSubDateTimeEntities(subDateTimeEntities); + + return result; + } + + // Parse specific TimeOfDay like "this night", "early morning", "late evening" + protected DateTimeResolutionResult parseSpecificTimeOfDay(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + String trimmedText = text.trim().toLowerCase(); + String timeText = trimmedText; + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getPeriodTimeOfDayWithDateRegex(), trimmedText)).findFirst(); + + // Extract early/late prefix from text if any + boolean hasEarly = false; + boolean hasLate = false; + if (match.isPresent()) { + timeText = match.get().getGroup("timeOfDay").value; + + if (!StringUtility.isNullOrEmpty(match.get().getGroup("early").value)) { + hasEarly = true; + result.setComment(Constants.Comment_Early); + result.setMod(Constants.EARLY_MOD); + } + + if (!hasEarly && !StringUtility.isNullOrEmpty(match.get().getGroup("late").value)) { + hasLate = true; + result.setComment(Constants.Comment_Late); + result.setMod(Constants.LATE_MOD); + } + } else { + match = Arrays.stream(RegExpUtility.getMatches(config.getAmDescRegex(), trimmedText)).findFirst(); + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getPmDescRegex(), trimmedText)).findFirst(); + } + + if (match.isPresent()) { + timeText = match.get().value; + } + } + + // Handle time of day + + String timeStr = null; + int beginHour = -1; + int endHour = -1; + int endMin = -1; + + // Late/early only works with time of day + // Only standard time of day (morinng, afternoon, evening and night) will not directly return + MatchedTimeRangeResult matchedTimeRange = config.getMatchedTimeRange(timeText, timeStr, beginHour, endHour, endMin); + timeStr = matchedTimeRange.getTimeStr(); + beginHour = matchedTimeRange.getBeginHour(); + endHour = matchedTimeRange.getEndHour(); + endMin = matchedTimeRange.getEndMin(); + + if (!matchedTimeRange.getMatched()) { + return result; + } + + // Modify time period if "early" or "late" exists + // Since 'time of day' is defined as four hour periods, + // the first 2 hours represent early, the later 2 hours represent late + if (hasEarly) { + endHour = beginHour + 2; + // Handling speical case: night ends with 23:59 + if (endMin == 59) { + endMin = 0; + } + } else if (hasLate) { + beginHour = beginHour + 2; + } + + if (RegexExtension.isExactMatch(config.getSpecificTimeOfDayRegex(), trimmedText, true)) { + int swift = config.getSwiftPrefix(trimmedText); + + LocalDateTime date = referenceDate.plusDays(swift); + int day = date.getDayOfMonth(); + int month = date.getMonthValue(); + int year = date.getYear(); + + result.setTimex(DateTimeFormatUtil.formatDate(date) + timeStr); + + Pair resultValue = new Pair( + DateUtil.safeCreateFromMinValue(year, month, day, beginHour, 0, 0), + DateUtil.safeCreateFromMinValue(year, month, day, endHour, endMin, endMin) + ); + + result.setFutureValue(resultValue); + result.setPastValue(resultValue); + + result.setSuccess(true); + + return result; + } + + // Handle Date followed by morning, afternoon and morning, afternoon followed by Date + match = Arrays.stream(RegExpUtility.getMatches(config.getPeriodTimeOfDayWithDateRegex(), trimmedText)).findFirst(); + + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getAmDescRegex(), trimmedText)).findFirst(); + + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getPmDescRegex(), trimmedText)).findFirst(); + } + } + + if (match.isPresent()) { + String beforeStr = trimmedText.substring(0, match.get().index).trim(); + String afterStr = trimmedText.substring(match.get().index + match.get().length).trim(); + + // Eliminate time period, if any + List timePeriodErs = config.getTimePeriodExtractor().extract(beforeStr); + if (timePeriodErs.size() > 0) { + beforeStr = beforeStr.substring(0, timePeriodErs.get(0).getStart()) + beforeStr.substring(timePeriodErs.get(0).getStart() + timePeriodErs.get(0).getLength()) + .trim(); + } else { + timePeriodErs = config.getTimePeriodExtractor().extract(afterStr); + if (timePeriodErs.size() > 0) { + afterStr = afterStr.substring(0, timePeriodErs.get(0).getStart()) + afterStr.substring(timePeriodErs.get(0).getStart() + timePeriodErs.get(0).getLength()) + .trim(); + } + } + + List ers = config.getDateExtractor().extract(beforeStr + " " + afterStr, referenceDate); + + if (ers.size() == 0 || ers.get(0).getLength() < beforeStr.length()) { + boolean valid = false; + + if (ers.size() > 0 && ers.get(0).getStart() == 0) { + String midStr = beforeStr.substring(ers.get(0).getStart() + ers.get(0).getLength()); + if (StringUtility.isNullOrWhiteSpace(midStr.replace(",", " "))) { + valid = true; + } + } + + if (!valid) { + ers = config.getDateExtractor().extract(afterStr, referenceDate); + + if (ers.size() == 0 || ers.get(0).getLength() != beforeStr.length()) { + if (ers.size() > 0 && ers.get(0).getStart() + ers.get(0).getLength() == afterStr.length()) { + String midStr = afterStr.substring(0, ers.get(0).getStart()); + if (StringUtility.isNullOrWhiteSpace(midStr.replace(",", " "))) { + valid = true; + } + } + } else { + valid = true; + } + } + + if (!valid) { + return result; + } + } + + boolean hasSpecificTimePeriod = false; + if (timePeriodErs.size() > 0) { + DateTimeParseResult timePr = config.getTimePeriodParser().parse(timePeriodErs.get(0), referenceDate); + if (timePr != null) { + Pair periodFuture = (Pair)((DateTimeResolutionResult)timePr.getValue()).getFutureValue(); + Pair periodPast = (Pair)((DateTimeResolutionResult)timePr.getValue()).getPastValue(); + + if (periodFuture == periodPast) { + beginHour = periodFuture.getValue0().getHour(); + endHour = periodFuture.getValue1().getHour(); + } else { + if (periodFuture.getValue0().getHour() >= beginHour || periodFuture.getValue1().getHour() <= endHour) { + beginHour = periodFuture.getValue0().getHour(); + endHour = periodFuture.getValue1().getHour(); + } else { + beginHour = periodPast.getValue0().getHour(); + endHour = periodPast.getValue1().getHour(); + } + } + + hasSpecificTimePeriod = true; + } + } + + DateTimeParseResult pr = config.getDateParser().parse(ers.get(0), referenceDate); + LocalDateTime futureDate = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getFutureValue(); + LocalDateTime pastDate = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getPastValue(); + + if (!hasSpecificTimePeriod) { + result.setTimex(pr.getTimexStr() + timeStr); + } else { + result.setTimex(String.format("(%sT%d,%sT%d,PT%dH)", pr.getTimexStr(), beginHour, pr.getTimexStr(), endHour, endHour - beginHour)); + } + + Pair futureResult = new Pair( + DateUtil.safeCreateFromMinValue( + futureDate.getYear(), futureDate.getMonthValue(), futureDate.getDayOfMonth(), + beginHour, 0, 0), + DateUtil.safeCreateFromMinValue( + futureDate.getYear(), futureDate.getMonthValue(), futureDate.getDayOfMonth(), + endHour, endMin, endMin) + ); + + Pair pastResult = new Pair( + DateUtil.safeCreateFromMinValue( + pastDate.getYear(), pastDate.getMonthValue(), pastDate.getDayOfMonth(), + beginHour, 0, 0), + DateUtil.safeCreateFromMinValue( + pastDate.getYear(), pastDate.getMonthValue(), pastDate.getDayOfMonth(), + endHour, endMin, endMin) + ); + + result.setFutureValue(futureResult); + result.setPastValue(pastResult); + + result.setSuccess(true); + + return result; + } + + return result; + } + + // TODO: this can be abstracted with the similar method in BaseDatePeriodParser + // Parse "in 20 minutes" + private DateTimeResolutionResult parseDuration(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + // For the rest of datetime, it will be handled in next function + if (RegExpUtility.getMatches(config.getRestOfDateTimeRegex(), text).length > 0) { + return result; + } + + List ers = config.getDurationExtractor().extract(text, referenceTime); + + if (ers.size() == 1) { + ParseResult pr = config.getDurationParser().parse(ers.get(0)); + + String beforeStr = text.substring(0, pr.getStart()).trim().toLowerCase(); + String afterStr = text.substring(pr.getStart() + pr.getLength()).trim().toLowerCase(); + + List numbersInSuffix = config.getCardinalExtractor().extract(beforeStr); + List numbersInDuration = config.getCardinalExtractor().extract(ers.get(0).getText()); + + // Handle cases like "2 upcoming days", "5 previous years" + if (!numbersInSuffix.isEmpty() && numbersInDuration.isEmpty()) { + ExtractResult numberEr = numbersInSuffix.get(0); + String numberText = numberEr.getText(); + String durationText = ers.get(0).getText(); + String combinedText = String.format("%s %s", numberText, durationText); + List combinedDurationEr = config.getDurationExtractor().extract(combinedText, referenceTime); + + if (!combinedDurationEr.isEmpty()) { + pr = config.getDurationParser().parse(combinedDurationEr.get(0)); + int startIndex = numberEr.getStart() + numberEr.getLength(); + beforeStr = beforeStr.substring(startIndex).trim(); + } + } + + if (pr.getValue() != null) { + int swiftSeconds = 0; + String mod = ""; + DateTimeResolutionResult durationResult = (DateTimeResolutionResult)pr.getValue(); + + if (durationResult.getPastValue() instanceof Double && durationResult.getFutureValue() instanceof Double) { + swiftSeconds = Math.round(((Double)durationResult.getPastValue()).floatValue()); + } + + LocalDateTime beginTime = referenceTime; + LocalDateTime endTime = referenceTime; + + if (RegexExtension.isExactMatch(config.getPastRegex(), beforeStr, true)) { + mod = Constants.BEFORE_MOD; + beginTime = referenceTime.minusSeconds(swiftSeconds); + } + + // Handle the "within (the) (next) xx seconds/minutes/hours" case + // Should also handle the multiple duration case like P1DT8H + // Set the beginTime equal to reference time for now + if (RegexExtension.isExactMatch(config.getWithinNextPrefixRegex(), beforeStr, true)) { + endTime = beginTime.plusSeconds(swiftSeconds); + } + + if (RegexExtension.isExactMatch(config.getFutureRegex(), beforeStr, true)) { + mod = Constants.AFTER_MOD; + endTime = beginTime.plusSeconds(swiftSeconds); + } + + if (RegexExtension.isExactMatch(config.getPastRegex(), afterStr, true)) { + mod = Constants.BEFORE_MOD; + beginTime = referenceTime.minusSeconds(swiftSeconds); + } + + if (RegexExtension.isExactMatch(config.getFutureRegex(), afterStr, true)) { + mod = Constants.AFTER_MOD; + endTime = beginTime.plusSeconds(swiftSeconds); + } + + if (RegexExtension.isExactMatch(config.getFutureSuffixRegex(), afterStr, true)) { + mod = Constants.AFTER_MOD; + endTime = beginTime.plusSeconds(swiftSeconds); + } + + result.setTimex(String.format("(%sT%s,%sT%s,%s)", + DateTimeFormatUtil.luisDate(beginTime), + DateTimeFormatUtil.luisTime(beginTime), + DateTimeFormatUtil.luisDate(endTime), + DateTimeFormatUtil.luisTime(endTime), + durationResult.getTimex() + )); + + Pair resultValue = new Pair(beginTime, endTime); + + result.setFutureValue(resultValue); + result.setPastValue(resultValue); + + result.setSuccess(true); + + if (!StringUtility.isNullOrEmpty(mod)) { + ((DateTimeResolutionResult)pr.getValue()).setMod(mod); + } + + List subDateTimeEntities = new ArrayList(); + subDateTimeEntities.add(pr); + result.setSubDateTimeEntities(subDateTimeEntities); + + return result; + } + } + + return result; + } + + // Parse "last minute", "next hour" + private DateTimeResolutionResult parseRelativeUnit(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getRelativeTimeUnitRegex(), text)).findFirst(); + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getRestOfDateTimeRegex(), text)).findFirst(); + } + + if (match.isPresent()) { + String srcUnit = match.get().getGroup("unit").value; + + String unitStr = config.getUnitMap().get(srcUnit); + + int swiftValue = 1; + Optional prefixMatch = Arrays.stream(RegExpUtility.getMatches(config.getPastRegex(), text)).findFirst(); + if (prefixMatch.isPresent()) { + swiftValue = -1; + } + + LocalDateTime beginTime = referenceDate; + LocalDateTime endTime = referenceDate; + String ptTimex = ""; + + if (config.getUnitMap().containsKey(srcUnit)) { + switch (unitStr) { + case "D": + endTime = DateUtil.safeCreateFromMinValue(beginTime.getYear(), beginTime.getMonthValue(), beginTime.getDayOfMonth()); + endTime = endTime.plusDays(1).minusSeconds(1); + ptTimex = String.format("PT%dS", ChronoUnit.SECONDS.between(beginTime, endTime)); + break; + case "H": + beginTime = swiftValue > 0 ? beginTime : referenceDate.plusHours(swiftValue); + endTime = swiftValue > 0 ? referenceDate.plusHours(swiftValue) : endTime; + ptTimex = "PT1H"; + break; + case "M": + beginTime = swiftValue > 0 ? beginTime : referenceDate.plusMinutes(swiftValue); + endTime = swiftValue > 0 ? referenceDate.plusMinutes(swiftValue) : endTime; + ptTimex = "PT1M"; + break; + case "S": + beginTime = swiftValue > 0 ? beginTime : referenceDate.plusSeconds(swiftValue); + endTime = swiftValue > 0 ? referenceDate.plusSeconds(swiftValue) : endTime; + ptTimex = "PT1S"; + break; + default: + return result; + } + + result.setTimex(String.format("(%sT%s,%sT%s,%s)", + DateTimeFormatUtil.luisDate(beginTime), + DateTimeFormatUtil.luisTime(beginTime), + DateTimeFormatUtil.luisDate(endTime), + DateTimeFormatUtil.luisTime(endTime), + ptTimex + )); + + Pair resultValue = new Pair(beginTime, endTime); + + result.setFutureValue(resultValue); + result.setPastValue(resultValue); + + result.setSuccess(true); + + return result; + } + } + + return result; + } + + private DateTimeResolutionResult parseDateWithPeriodPrefix(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + List dateResult = config.getDateExtractor().extract(text); + if (dateResult.size() > 0) { + String beforeStr = StringUtility.trimEnd(text.substring(0, dateResult.get(dateResult.size() - 1).getStart())); + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getPrefixDayRegex(), beforeStr)).findFirst(); + if (match.isPresent()) { + DateTimeParseResult pr = config.getDateParser().parse(dateResult.get(dateResult.size() - 1), referenceDate); + if (pr.getValue() != null) { + LocalDateTime startTime = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getFutureValue(); + startTime = LocalDateTime.of(startTime.getYear(), startTime.getMonthValue(), startTime.getDayOfMonth(), 0, 0, 0); + LocalDateTime endTime = startTime; + + if (!StringUtility.isNullOrEmpty(match.get().getGroup("EarlyPrefix").value)) { + endTime = endTime.plusHours(Constants.HalfDayHourCount); + result.setMod(Constants.EARLY_MOD); + } else if (!StringUtility.isNullOrEmpty(match.get().getGroup("MidPrefix").value)) { + startTime = startTime.plusHours(Constants.HalfDayHourCount - Constants.HalfMidDayDurationHourCount); + endTime = endTime.plusHours(Constants.HalfDayHourCount + Constants.HalfMidDayDurationHourCount); + result.setMod(Constants.MID_MOD); + } else if (!StringUtility.isNullOrEmpty(match.get().getGroup("LatePrefix").value)) { + startTime = startTime.plusHours(Constants.HalfDayHourCount); + endTime = startTime.plusHours(Constants.HalfDayHourCount); + result.setMod(Constants.LATE_MOD); + } else { + return result; + } + + result.setTimex(pr.getTimexStr()); + + Pair resultValue = new Pair(startTime, endTime); + + result.setFutureValue(resultValue); + result.setPastValue(resultValue); + + result.setSuccess(true); + } + } + } + + return result; + } + + private DateTimeResolutionResult parseDateWithTimePeriodSuffix(String text, LocalDateTime referenceDate) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + Optional dateEr = config.getDateExtractor().extract(text).stream().findFirst(); + Optional timeEr = config.getTimeExtractor().extract(text).stream().findFirst(); + + if (dateEr.isPresent() && timeEr.isPresent()) { + int dateStrEnd = dateEr.get().getStart() + dateEr.get().getLength(); + + if (dateStrEnd < timeEr.get().getStart()) { + String midStr = text.substring(dateStrEnd, timeEr.get().getStart()); + + if (isValidConnectorForDateAndTimePeriod(midStr)) { + DateTimeParseResult datePr = config.getDateParser().parse(dateEr.get(), referenceDate); + DateTimeParseResult timePr = config.getTimeParser().parse(timeEr.get(), referenceDate); + + if (datePr != null && timePr != null) { + DateTimeResolutionResult timeResolutionResult = (DateTimeResolutionResult)timePr.getValue(); + DateTimeResolutionResult dateResolutionResult = (DateTimeResolutionResult)datePr.getValue(); + LocalDateTime futureDateValue = (LocalDateTime)dateResolutionResult.getFutureValue(); + LocalDateTime pastDateValue = (LocalDateTime)dateResolutionResult.getPastValue(); + LocalDateTime futureTimeValue = (LocalDateTime)timeResolutionResult.getFutureValue(); + LocalDateTime pastTimeValue = (LocalDateTime)timeResolutionResult.getPastValue(); + + result.setComment(timeResolutionResult.getComment()); + result.setTimex(datePr.getTimexStr() + timePr.getTimexStr()); + + result.setFutureValue(DateUtil.safeCreateFromMinValue(futureDateValue.toLocalDate(), futureTimeValue.toLocalTime())); + result.setPastValue(DateUtil.safeCreateFromMinValue(pastDateValue.toLocalDate(), pastTimeValue.toLocalTime())); + + if (RegExpUtility.getMatches(config.getBeforeRegex(), midStr).length > 0) { + result.setMod(Constants.BEFORE_MOD); + } else { + result.setMod(Constants.AFTER_MOD); + } + + List subDateTimeEntities = new ArrayList<>(); + subDateTimeEntities.add(datePr); + subDateTimeEntities.add(timePr); + + result.setSubDateTimeEntities(subDateTimeEntities); + + result.setSuccess(true); + } + } + } + } + + return result; + } + + // Cases like "today after 2:00pm", "1/1/2015 before 2:00 in the afternoon" + // Valid connector in English for Before include: "before", "no later than", "in advance of", "prior to", "earlier than", "sooner than", "by", "till", "until"... + // Valid connector in English for After include: "after", "later than" + private boolean isValidConnectorForDateAndTimePeriod(String text) { + List beforeAfterRegexes = new ArrayList<>(); + beforeAfterRegexes.add(config.getBeforeRegex()); + beforeAfterRegexes.add(config.getAfterRegex()); + text = text.trim(); + + for (Pattern regex : beforeAfterRegexes) { + ConditionalMatch match = RegexExtension.matchExact(regex, text, true); + if (match.getSuccess()) { + return true; + } + } + + return false; + } + + private class ParseTimePeriodResult { + String comments; + int beginHour; + int endHour; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDurationParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDurationParser.java new file mode 100644 index 000000000..85bb606b4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseDurationParser.java @@ -0,0 +1,414 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.IDurationParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +public class BaseDurationParser implements IDateTimeParser { + + private final IDurationParserConfiguration config; + + public BaseDurationParser(IDurationParserConfiguration configuration) { + this.config = configuration; + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_DURATION; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + Object value = null; + + if (er.getType().equals(getParserName())) { + DateTimeResolutionResult innerResult; + + innerResult = parseMergedDuration(er.getText(), reference); + + if (!innerResult.getSuccess()) { + innerResult = parseNumberWithUnit(er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseImplicitDuration(er.getText(), reference); + } + + if (innerResult.getSuccess()) { + innerResult.setFutureResolution(ImmutableMap.builder() + .put(TimeTypeConstants.DURATION, StringUtility.format((Double)innerResult.getFutureValue())) + .build()); + + innerResult.setPastResolution(ImmutableMap.builder() + .put(TimeTypeConstants.DURATION, StringUtility.format((Double)innerResult.getPastValue())) + .build()); + + if (er.getData() != null) { + if (er.getData().equals(Constants.MORE_THAN_MOD)) { + innerResult.setMod(Constants.MORE_THAN_MOD); + } else if (er.getData().equals(Constants.LESS_THAN_MOD)) { + innerResult.setMod(Constants.LESS_THAN_MOD); + } + } + + value = innerResult; + } + } + + DateTimeParseResult result = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex() + ); + + return result; + } + + private DateTimeResolutionResult parseMergedDuration(String text, LocalDateTime reference) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + IExtractor durationExtractor = config.getDurationExtractor(); + + // DurationExtractor without parameter will not extract merged duration + List ers = durationExtractor.extract(text); + + // only handle merged duration cases like "1 month 21 days" + if (ers.size() <= 1) { + result.setSuccess(false); + return result; + } + + int start = ers.get(0).getStart(); + if (start != 0) { + String beforeStr = text.substring(0, start - 1); + if (!StringUtility.isNullOrWhiteSpace(beforeStr)) { + return result; + } + } + + int end = ers.get(ers.size() - 1).getStart() + ers.get(ers.size() - 1).getLength(); + if (end != text.length()) { + String afterStr = text.substring(end); + if (!StringUtility.isNullOrWhiteSpace(afterStr)) { + return result; + } + } + + List prs = new ArrayList<>(); + Map timexMap = new HashMap<>(); + + // insert timex into a dictionary + for (ExtractResult er : ers) { + Pattern unitRegex = config.getDurationUnitRegex(); + Optional unitMatch = Arrays.stream(RegExpUtility.getMatches(unitRegex, er.getText())).findFirst(); + if (unitMatch.isPresent()) { + DateTimeParseResult pr = (DateTimeParseResult)parse(er); + if (pr.getValue() != null) { + timexMap.put(unitMatch.get().getGroup("unit").value, pr.getTimexStr()); + prs.add(pr); + } + } + } + + // sort the timex using the granularity of the duration, "P1M23D" for "1 month 23 days" and "23 days 1 month" + if (prs.size() == ers.size()) { + + result.setTimex(TimexUtility.generateCompoundDurationTimex(timexMap, config.getUnitValueMap())); + + double value = 0; + for (DateTimeParseResult pr : prs) { + value += Double.parseDouble(((DateTimeResolutionResult)pr.getValue()).getFutureValue().toString()); + } + + result.setFutureValue(value); + result.setPastValue(value); + } + + result.setSuccess(true); + return result; + } + + private DateTimeResolutionResult parseNumberWithUnit(String text, LocalDateTime reference) { + DateTimeResolutionResult result = parseNumberSpaceUnit(text); + if (!result.getSuccess()) { + result = parseNumberCombinedUnit(text); + } + + if (!result.getSuccess()) { + result = parseAnUnit(text); + } + + if (!result.getSuccess()) { + result = parseInexactNumberUnit(text); + } + + return result; + } + + // check {and} suffix after a {number} {unit} + private double parseNumberWithUnitAndSuffix(String text) { + double numVal = 0; + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getSuffixAndRegex(), text)).findFirst(); + if (match.isPresent()) { + String numStr = match.get().getGroup("suffix_num").value.toLowerCase(); + + if (config.getDoubleNumbers().containsKey(numStr)) { + numVal = config.getDoubleNumbers().get(numStr); + } + } + + return numVal; + } + + private DateTimeResolutionResult parseNumberSpaceUnit(String text) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + // if there are spaces between nubmer and unit + List ers = config.getCardinalExtractor().extract(text); + if (ers.size() == 1) { + ExtractResult er = ers.get(0); + ParseResult pr = config.getNumberParser().parse(er); + + // followed unit: {num} (and a half hours) + String srcUnit = ""; + String noNum = text.substring(er.getStart() + er.getLength()).trim().toLowerCase(); + String suffixStr = text; + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getFollowedUnit(), noNum)).findFirst(); + if (match.isPresent()) { + srcUnit = match.get().getGroup("unit").value.toLowerCase(); + suffixStr = match.get().getGroup(Constants.SuffixGroupName).value.toLowerCase(); + } + + if (match.isPresent() && !StringUtility.isNullOrEmpty(match.get().getGroup(Constants.BusinessDayGroupName).value)) { + int numVal = Math.round(Double.valueOf(pr.getValue().toString()).floatValue()); + + String timex = TimexUtility.generateDurationTimex(numVal, Constants.TimexBusinessDay, false); + double timeValue = numVal * config.getUnitValueMap().get(srcUnit.split(" ")[1]); + + result.setTimex(timex); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + } + + if (config.getUnitMap().containsKey(srcUnit)) { + double numVal = Double.parseDouble(pr.getValue().toString()) + parseNumberWithUnitAndSuffix(suffixStr); + + String unitStr = config.getUnitMap().get(srcUnit); + + String timex = TimexUtility.generateDurationTimex(numVal, unitStr, isLessThanDay(unitStr)); + double timeValue = numVal * config.getUnitValueMap().get(srcUnit); + + result.setTimex(timex); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + } + } + + return result; + } + + private DateTimeResolutionResult parseNumberCombinedUnit(String text) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + String suffixStr = text; + + // if there are NO spaces between number and unit + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getNumberCombinedWithUnit(), text)).findFirst(); + if (match.isPresent()) { + Double numVal = Double.parseDouble(match.get().getGroup("num").value) + parseNumberWithUnitAndSuffix(suffixStr); + String numStr = StringUtility.format(numVal); + + String srcUnit = match.get().getGroup("unit").value.toLowerCase(); + + if (config.getUnitMap().containsKey(srcUnit)) { + String unitStr = config.getUnitMap().get(srcUnit); + + if ((numVal > 1000) && (unitStr.equals("Y") || unitStr.equals("MON") || unitStr.equals("W"))) { + return result; + } + + String timex = String.format("P%s%s%c", isLessThanDay(unitStr) ? "T" : "", numStr, unitStr.charAt(0)); + double timeValue = numVal * config.getUnitValueMap().get(srcUnit); + + result.setTimex(timex); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + return result; + } + } + + return result; + } + + private DateTimeResolutionResult parseAnUnit(String text) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + String suffixStr = text; + + // if there are NO spaces between number and unit + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getAnUnitRegex(), text)).findFirst(); + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getHalfDateUnitRegex(), text)).findFirst(); + } + + if (match.isPresent()) { + double numVal = StringUtility.isNullOrEmpty(match.get().getGroup("half").value) ? 1 : 0.5; + numVal += parseNumberWithUnitAndSuffix(suffixStr); + String numStr = StringUtility.format(numVal); + + String srcUnit = match.get().getGroup("unit").value.toLowerCase(); + + if (config.getUnitMap().containsKey(srcUnit)) { + String unitStr = config.getUnitMap().get(srcUnit); + + double timeValue = numVal * config.getUnitValueMap().get(srcUnit); + + result.setTimex(TimexUtility.generateDurationTimex(numVal, unitStr, isLessThanDay(unitStr))); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + + } else if (!StringUtility.isNullOrEmpty(match.get().getGroup(Constants.BusinessDayGroupName).value)) { + String timex = TimexUtility.generateDurationTimex(numVal, Constants.TimexBusinessDay, false); + double timeValue = numVal * config.getUnitValueMap().get(srcUnit.split(" ")[1]); + + result.setTimex(timex); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + } + } + + return result; + } + + private DateTimeResolutionResult parseInexactNumberUnit(String text) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getInexactNumberUnitRegex(), text)).findFirst(); + if (match.isPresent()) { + double numVal; + + if (!StringUtility.isNullOrEmpty(match.get().getGroup("NumTwoTerm").value)) { + numVal = 2; + } else { + // set the inexact number "few", "some" to 3 for now + numVal = 3; + } + + String numStr = StringUtility.format(numVal); + + String srcUnit = match.get().getGroup("unit").value.toLowerCase(); + + if (config.getUnitMap().containsKey(srcUnit)) { + String unitStr = config.getUnitMap().get(srcUnit); + + String timex = String.format("P%s%s%c", isLessThanDay(unitStr) ? "T" : "", numStr, unitStr.charAt(0)); + double timeValue = numVal * config.getUnitValueMap().get(srcUnit); + + result.setTimex(timex); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + return result; + } + } + + return result; + } + + private DateTimeResolutionResult parseImplicitDuration(String text, LocalDateTime reference) { + // handle "all day" "all year" + DateTimeResolutionResult result = getResultFromRegex(config.getAllDateUnitRegex(), text, "1"); + + // handle "during/for the day/week/month/year" + if (config.getOptions().match(DateTimeOptions.CalendarMode) && !result.getSuccess()) { + result = getResultFromRegex(config.getDuringRegex(), text, "1"); + } + + // handle "half day", "half year" + if (!result.getSuccess()) { + result = getResultFromRegex(config.getHalfDateUnitRegex(), text, "0.5"); + } + + // handle single duration unit, it is filtered in the extraction that there is a relative word in advance + if (!result.getSuccess()) { + result = getResultFromRegex(config.getFollowedUnit(), text, "1"); + } + + return result; + } + + private DateTimeResolutionResult getResultFromRegex(Pattern pattern, String text, String numStr) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + Optional match = Arrays.stream(RegExpUtility.getMatches(pattern, text)).findFirst(); + if (match.isPresent()) { + String srcUnit = match.get().getGroup("unit").value.toLowerCase(); + if (config.getUnitMap().containsKey(srcUnit)) { + String unitStr = config.getUnitMap().get(srcUnit); + + String timex = String.format("P%s%s%c", isLessThanDay(unitStr) ? "T" : "", numStr, unitStr.charAt(0)); + double timeValue = Double.parseDouble(numStr) * config.getUnitValueMap().get(srcUnit); + + result.setTimex(timex); + result.setFutureValue(timeValue); + result.setPastValue(timeValue); + + result.setSuccess(true); + } + } + + return result; + } + + private boolean isLessThanDay(String unit) { + return unit.equals("S") || unit.equals("M") || unit.equals("H"); + } + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseHolidayParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseHolidayParser.java new file mode 100644 index 000000000..568ba9905 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseHolidayParser.java @@ -0,0 +1,204 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import static java.lang.Integer.parseInt; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.IHolidayParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.function.IntFunction; +import java.util.regex.Pattern; +import java.util.stream.StreamSupport; + +public class BaseHolidayParser implements IDateTimeParser { + + private final IHolidayParserConfiguration config; + + public BaseHolidayParser(IHolidayParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_DATE; + } + + @Override + public List filterResults(String query, List candidateResults) { + throw new UnsupportedOperationException(); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + LocalDateTime referenceDate = reference; + Object value = null; + + if (er.getType().equals(getParserName())) { + + DateTimeResolutionResult innerResult = parseHolidayRegexMatch(er.getText(), referenceDate); + + if (innerResult.getSuccess()) { + HashMap futureResolution = new HashMap<>(); + futureResolution.put(TimeTypeConstants.DATE, DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getFutureValue())); + innerResult.setFutureResolution(futureResolution); + + HashMap pastResolution = new HashMap<>(); + pastResolution.put(TimeTypeConstants.DATE, DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getPastValue())); + innerResult.setPastResolution(pastResolution); + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex() + ); + + return ret; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + private DateTimeResolutionResult parseHolidayRegexMatch(String text, LocalDateTime referenceDate) { + + for (Pattern pattern : this.config.getHolidayRegexList()) { + ConditionalMatch match = RegexExtension.matchExact(pattern, text, true); + if (match.getSuccess()) { + // LUIS value string will be set in Match2Date method + DateTimeResolutionResult ret = match2Date(match.getMatch().get(), referenceDate); + + return ret; + } + } + + return new DateTimeResolutionResult(); + } + + private DateTimeResolutionResult match2Date(Match match, LocalDateTime referenceDate) { + + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + String holidayStr = this.config.sanitizeHolidayToken(match.getGroup("holiday").value.toLowerCase(Locale.ROOT)); + + // get year (if exist) + String yearStr = match.getGroup("year").value.toLowerCase(); + String orderStr = match.getGroup("order").value.toLowerCase(); + int year; + boolean hasYear = false; + + if (!StringUtility.isNullOrEmpty(yearStr)) { + year = parseInt(yearStr); + hasYear = true; + } else if (!StringUtility.isNullOrEmpty(orderStr)) { + int swift = this.config.getSwiftYear((orderStr)); + if (swift < -1) { + return ret; + } + + year = referenceDate.getYear() + swift; + hasYear = true; + } else { + year = referenceDate.getYear(); + } + + String holidayKey = ""; + for (ImmutableMap.Entry> holidayPair : this.config.getHolidayNames().entrySet()) { + if (StreamSupport.stream(holidayPair.getValue().spliterator(), false).anyMatch(name -> holidayStr.equals(name))) { + holidayKey = holidayPair.getKey(); + break; + } + } + + String timexStr = ""; + if (!StringUtility.isNullOrEmpty(holidayKey)) { + + LocalDateTime value = referenceDate; + IntFunction function = this.config.getHolidayFuncDictionary().get(holidayKey); + if (function != null) { + + value = function.apply(year); + + timexStr = this.config.getVariableHolidaysTimexDictionary().get(holidayKey); + if (StringUtility.isNullOrEmpty(timexStr)) { + timexStr = String.format("-%02d-%02d", value.getMonthValue(), value.getDayOfMonth()); + } + } + + if (function == null) { + return ret; + } + + if (value.equals(DateUtil.minValue())) { + ret.setTimex(""); + ret.setPastValue(DateUtil.minValue()); + ret.setFutureValue(DateUtil.minValue()); + ret.setSuccess(true); + return ret; + } + + if (hasYear) { + ret.setTimex(String.format("%04d", year) + timexStr); + ret.setFutureValue(DateUtil.safeCreateFromMinValue(year, value.getMonthValue(), value.getDayOfMonth())); + ret.setPastValue(DateUtil.safeCreateFromMinValue(year, value.getMonthValue(), value.getDayOfMonth())); + ret.setSuccess(true); + return ret; + } + + ret.setTimex("XXXX" + timexStr); + ret.setFutureValue(getFutureValue(value, referenceDate, holidayKey)); + ret.setPastValue(getPastValue(value, referenceDate, holidayKey)); + ret.setSuccess(true); + + return ret; + } + + return ret; + } + + private LocalDateTime getFutureValue(LocalDateTime value, LocalDateTime referenceDate, String holiday) { + + if (value.isBefore(referenceDate)) { + IntFunction function = this.config.getHolidayFuncDictionary().get(holiday); + if (function != null) { + return function.apply(value.getYear() + 1); + } + } + + return value; + } + + private LocalDateTime getPastValue(LocalDateTime value, LocalDateTime referenceDate, String holiday) { + + if (value.isAfter(referenceDate) || value == referenceDate) { + IntFunction function = this.config.getHolidayFuncDictionary().get(holiday); + if (function != null) { + return function.apply(value.getYear() - 1); + } + } + + return value; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseHolidayParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseHolidayParserConfiguration.java new file mode 100644 index 000000000..d8a7cfc1d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseHolidayParserConfiguration.java @@ -0,0 +1,163 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IHolidayParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.HashMap; +import java.util.function.IntFunction; +import java.util.regex.Pattern; + +public abstract class BaseHolidayParserConfiguration extends BaseOptionsConfiguration implements IHolidayParserConfiguration { + + private ImmutableMap variableHolidaysTimexDictionary; + + public final ImmutableMap getVariableHolidaysTimexDictionary() { + return variableHolidaysTimexDictionary; + } + + protected final void setVariableHolidaysTimexDictionary(ImmutableMap value) { + variableHolidaysTimexDictionary = value; + } + + private ImmutableMap> holidayFuncDictionary; + + public final ImmutableMap> getHolidayFuncDictionary() { + return holidayFuncDictionary; + } + + protected final void setHolidayFuncDictionary(ImmutableMap> value) { + holidayFuncDictionary = value; + } + + private ImmutableMap> holidayNames; + + public final ImmutableMap> getHolidayNames() { + return holidayNames; + } + + protected final void setHolidayNames(ImmutableMap> value) { + holidayNames = value; + } + + private Iterable holidayRegexList; + + public final Iterable getHolidayRegexList() { + return holidayRegexList; + } + + protected final void setHolidayRegexList(Iterable value) { + holidayRegexList = value; + } + + protected BaseHolidayParserConfiguration() { + super(DateTimeOptions.None); + this.variableHolidaysTimexDictionary = BaseDateTime.VariableHolidaysTimexDictionary; + this.setHolidayFuncDictionary(ImmutableMap.copyOf(initHolidayFuncs())); + } + + protected HashMap> initHolidayFuncs() { + HashMap> holidays = new HashMap<>(); + holidays.put("labour", BaseHolidayParserConfiguration::labourDay); + holidays.put("fathers", BaseHolidayParserConfiguration::fathersDay); + holidays.put("mothers", BaseHolidayParserConfiguration::mothersDay); + holidays.put("canberra", BaseHolidayParserConfiguration::canberraDay); + holidays.put("columbus", BaseHolidayParserConfiguration::columbusDay); + holidays.put("memorial", BaseHolidayParserConfiguration::memorialDay); + holidays.put("thanksgiving", BaseHolidayParserConfiguration::thanksgivingDay); + holidays.put("thanksgivingday", BaseHolidayParserConfiguration::thanksgivingDay); + holidays.put("blackfriday", BaseHolidayParserConfiguration::blackFriday); + holidays.put("martinlutherking", BaseHolidayParserConfiguration::martinLutherKingDay); + holidays.put("washingtonsbirthday", BaseHolidayParserConfiguration::washingtonsBirthday); + + return holidays; + } + + public abstract int getSwiftYear(String text); + + public abstract String sanitizeHolidayToken(String holiday); + + // @TODO auto-generate from YAML + private static LocalDateTime canberraDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, getDay(year, 3, 0, DayOfWeek.MONDAY)); + } + + private static LocalDateTime martinLutherKingDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 1, getDay(year, 1, 2, DayOfWeek.MONDAY)); + } + + private static LocalDateTime washingtonsBirthday(int year) { + return DateUtil.safeCreateFromMinValue(year, 2, getDay(year, 2, 2, DayOfWeek.MONDAY)); + } + + protected static LocalDateTime mothersDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 5, getDay(year, 5, 1, DayOfWeek.SUNDAY)); + } + + protected static LocalDateTime fathersDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 6, getDay(year, 6, 2, DayOfWeek.SUNDAY)); + } + + protected static LocalDateTime memorialDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 5, getLastDay(year, 5, DayOfWeek.MONDAY)); + } + + protected static LocalDateTime labourDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 9, getDay(year, 9, 0, DayOfWeek.MONDAY)); + } + + protected static LocalDateTime internationalWorkersDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 5, 1); + } + + protected static LocalDateTime columbusDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 10, getDay(year, 10, 1, DayOfWeek.MONDAY)); + } + + protected static LocalDateTime thanksgivingDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, getDay(year, 11, 3, DayOfWeek.THURSDAY)); + } + + protected static LocalDateTime blackFriday(int year) { + return DateUtil.safeCreateFromMinValue(year, 11, getDay(year, 11, 3, DayOfWeek.FRIDAY)); + } + + protected static int getDay(int year, int month, int week, DayOfWeek dayOfWeek) { + + YearMonth yearMonthObject = YearMonth.of(year, month); + int daysInMonth = yearMonthObject.lengthOfMonth(); + + int weekCount = 0; + for (int day = 1; day < daysInMonth + 1; day++) { + if (DateUtil.safeCreateFromMinValue(year, month, day).getDayOfWeek() == dayOfWeek) { + weekCount++; + if (weekCount == week + 1) { + return day; + } + } + } + + throw new Error("day out of bound."); + } + + protected static int getLastDay(int year, int month, DayOfWeek dayOfWeek) { + + YearMonth yearMonthObject = YearMonth.of(year, month); + int daysInMonth = yearMonthObject.lengthOfMonth(); + + int lastDay = 0; + for (int day = 1; day < daysInMonth + 1; day++) { + if (DateUtil.safeCreateFromMinValue(year, month, day).getDayOfWeek() == dayOfWeek) { + lastDay = day; + } + } + + return lastDay; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseMergedDateTimeParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseMergedDateTimeParser.java new file mode 100644 index 000000000..434d5372b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseMergedDateTimeParser.java @@ -0,0 +1,834 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.ResolutionKey; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DatePeriodTimexType; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.DateTimeResolutionKey; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.IMergedParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.MatchingUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BaseMergedDateTimeParser implements IDateTimeParser { + + private final String parserName = "datetimeV2"; + private final IMergedParserConfiguration config; + private static final String dateMinString = DateTimeFormatUtil.formatDate(DateUtil.minValue()); + private static final String dateTimeMinString = DateTimeFormatUtil.formatDateTime(DateUtil.minValue()); + //private static final Calendar Cal = DateTimeFormatInfo.InvariantInfo.Calendar; + + public BaseMergedDateTimeParser(IMergedParserConfiguration config) { + this.config = config; + } + + public String getDateMinString() { + return dateMinString; + } + + public String getDateTimeMinString() { + return dateTimeMinString; + } + + @Override + public String getParserName() { + return parserName; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + DateTimeParseResult pr = null; + + String originText = er.getText(); + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + String newText = MatchingUtil.preProcessTextRemoveSuperfluousWords(er.getText(), config.getSuperfluousWordMatcher()).getText(); + int newLength = er.getLength() + er.getText().length() - originText.length(); + er = new ExtractResult(er.getStart(), newLength, newText, er.getType(), er.getData(), er.getMetadata()); + } + + // Push, save the MOD string + boolean hasBefore = false; + boolean hasAfter = false; + boolean hasSince = false; + boolean hasAround = false; + boolean hasYearAfter = false; + + // "InclusiveModifier" means MOD should include the start/end time + // For example, cases like "on or later than", "earlier than or in" have inclusive modifier + boolean hasInclusiveModifier = false; + String modStr = ""; + + if (er.getMetadata() != null && er.getMetadata().getHasMod()) { + ConditionalMatch beforeMatch = RegexExtension.matchBegin(config.getBeforeRegex(), er.getText(), true); + ConditionalMatch afterMatch = RegexExtension.matchBegin(config.getAfterRegex(), er.getText(), true); + ConditionalMatch sinceMatch = RegexExtension.matchBegin(config.getSinceRegex(), er.getText(), true); + ConditionalMatch aroundMatch = RegexExtension.matchBegin(config.getAroundRegex(), er.getText(), true); + + if (beforeMatch.getSuccess()) { + hasBefore = true; + er.setStart(er.getStart() + beforeMatch.getMatch().get().length); + er.setLength(er.getLength() - beforeMatch.getMatch().get().length); + er.setText(er.getText().substring(beforeMatch.getMatch().get().length)); + modStr = beforeMatch.getMatch().get().value; + + if (!StringUtility.isNullOrEmpty(beforeMatch.getMatch().get().getGroup("include").value)) { + hasInclusiveModifier = true; + } + } else if (afterMatch.getSuccess()) { + hasAfter = true; + er.setStart(er.getStart() + afterMatch.getMatch().get().length); + er.setLength(er.getLength() - afterMatch.getMatch().get().length); + er.setText(er.getText().substring(afterMatch.getMatch().get().length)); + modStr = afterMatch.getMatch().get().value; + + if (!StringUtility.isNullOrEmpty(afterMatch.getMatch().get().getGroup("include").value)) { + hasInclusiveModifier = true; + } + } else if (sinceMatch.getSuccess()) { + hasSince = true; + er.setStart(er.getStart() + sinceMatch.getMatch().get().length); + er.setLength(er.getLength() - sinceMatch.getMatch().get().length); + er.setText(er.getText().substring(sinceMatch.getMatch().get().length)); + modStr = sinceMatch.getMatch().get().value; + } else if (aroundMatch.getSuccess()) { + hasAround = true; + er.setStart(er.getStart() + aroundMatch.getMatch().get().length); + er.setLength(er.getLength() - aroundMatch.getMatch().get().length); + er.setText(er.getText().substring(aroundMatch.getMatch().get().length)); + modStr = aroundMatch.getMatch().get().value; + } else if ((er.getType().equals(Constants.SYS_DATETIME_DATEPERIOD) && + Arrays.stream(RegExpUtility.getMatches(config.getYearRegex(), er.getText())).findFirst().isPresent()) || + (er.getType().equals(Constants.SYS_DATETIME_DATE)) || (er.getType().equals(Constants.SYS_DATETIME_TIME))) { + // This has to be put at the end of the if, or cases like "before 2012" and "after 2012" would fall into this + // 2012 or after/above, 3 pm or later + ConditionalMatch match = RegexExtension.matchEnd(config.getSuffixAfterRegex(), er.getText(), true); + if (match.getSuccess()) { + hasYearAfter = true; + er.setLength(er.getLength() - match.getMatch().get().length); + er.setText(er.getLength() > 0 ? er.getText().substring(0, er.getLength()) : ""); + modStr = match.getMatch().get().value; + } + } + } + + if (er.getType().equals(Constants.SYS_DATETIME_DATE)) { + if (er.getMetadata() != null && er.getMetadata().getIsHoliday()) { + pr = config.getHolidayParser().parse(er, reference); + } else { + pr = this.config.getDateParser().parse(er, reference); + } + } else if (er.getType().equals(Constants.SYS_DATETIME_TIME)) { + pr = this.config.getTimeParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_DATETIME)) { + pr = this.config.getDateTimeParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_DATEPERIOD)) { + pr = this.config.getDatePeriodParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_TIMEPERIOD)) { + pr = this.config.getTimePeriodParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_DATETIMEPERIOD)) { + pr = this.config.getDateTimePeriodParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_DURATION)) { + pr = this.config.getDurationParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_SET)) { + pr = this.config.getGetParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_DATETIMEALT)) { + pr = this.config.getDateTimeAltParser().parse(er, reference); + } else if (er.getType().equals(Constants.SYS_DATETIME_TIMEZONE)) { + if (config.getOptions().match(DateTimeOptions.EnablePreview)) { + pr = this.config.getTimeZoneParser().parse(er, reference); + } + } else { + return null; + } + + if (pr == null) { + return null; + } + + // Pop, restore the MOD string + if (hasBefore && pr != null && pr.getValue() != null) { + + pr.setStart(pr.getStart() - modStr.length()); + pr.setText(modStr + pr.getText()); + pr.setLength(pr.getLength() + modStr.length()); + + DateTimeResolutionResult val = (DateTimeResolutionResult)pr.getValue(); + + if (!hasInclusiveModifier) { + val.setMod(combineMod(val.getMod(), Constants.BEFORE_MOD)); + } else { + val.setMod(combineMod(val.getMod(), Constants.UNTIL_MOD)); + } + + pr.setValue(val); + } + + if (hasAfter && pr != null && pr.getValue() != null) { + + pr.setStart(pr.getStart() - modStr.length()); + pr.setText(modStr + pr.getText()); + pr.setLength(pr.getLength() + modStr.length()); + + DateTimeResolutionResult val = (DateTimeResolutionResult)pr.getValue(); + + if (!hasInclusiveModifier) { + val.setMod(combineMod(val.getMod(), Constants.AFTER_MOD)); + } else { + val.setMod(combineMod(val.getMod(), Constants.SINCE_MOD)); + } + + pr.setValue(val); + } + + if (hasSince && pr != null && pr.getValue() != null) { + + pr.setStart(pr.getStart() - modStr.length()); + pr.setText(modStr + pr.getText()); + pr.setLength(pr.getLength() + modStr.length()); + + DateTimeResolutionResult val = (DateTimeResolutionResult)pr.getValue(); + val.setMod(combineMod(val.getMod(), Constants.SINCE_MOD)); + pr.setValue(val); + } + + if (hasAround && pr != null && pr.getValue() != null) { + + pr.setStart(pr.getStart() - modStr.length()); + pr.setText(modStr + pr.getText()); + pr.setLength(pr.getLength() + modStr.length()); + + DateTimeResolutionResult val = (DateTimeResolutionResult)pr.getValue(); + val.setMod(combineMod(val.getMod(), Constants.APPROX_MOD)); + pr.setValue(val); + } + + if (hasYearAfter && pr != null && pr.getValue() != null) { + + pr.setText(pr.getText() + modStr); + pr.setLength(pr.getLength() + modStr.length()); + + DateTimeResolutionResult val = (DateTimeResolutionResult)pr.getValue(); + val.setMod(combineMod(val.getMod(), Constants.SINCE_MOD)); + pr.setValue(val); + hasSince = true; + } + + // For cases like "3 pm or later on Monday" + if (pr != null && pr.getValue() != null && pr.getType().equals(Constants.SYS_DATETIME_DATETIME)) { + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getSuffixAfterRegex(), pr.getText())).findFirst(); + if (match.isPresent() && match.get().index != 0) { + DateTimeResolutionResult val = (DateTimeResolutionResult)pr.getValue(); + val.setMod(combineMod(val.getMod(), Constants.SINCE_MOD)); + pr.setValue(val); + hasSince = true; + } + } + + if (config.getOptions().match(DateTimeOptions.SplitDateAndTime) && pr != null && pr.getValue() != null && + ((DateTimeResolutionResult)pr.getValue()).getSubDateTimeEntities() != null) { + pr.setValue(dateTimeResolutionForSplit(pr)); + } else { + boolean hasModifier = hasBefore || hasAfter || hasSince; + if (pr.getValue() != null) { + ((DateTimeResolutionResult)pr.getValue()).setHasRangeChangingMod(hasModifier); + } + + pr = setParseResult(pr, hasModifier); + } + + // In this version, ExperimentalMode only cope with the "IncludePeriodEnd" case + if (this.config.getOptions().match(DateTimeOptions.ExperimentalMode)) { + if (pr.getMetadata() != null && pr.getMetadata().getIsPossiblyIncludePeriodEnd()) { + pr = setInclusivePeriodEnd(pr); + } + } + + if (this.config.getOptions().match(DateTimeOptions.EnablePreview)) { + int prLength = pr.getLength() + originText.length() - pr.getText().length(); + pr = new DateTimeParseResult(pr.getStart(), prLength, originText, pr.getType(), pr.getData(), pr.getValue(), pr.getResolutionStr(), pr.getTimexStr()); + } + + return pr; + } + + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } + + private boolean filterResultsPredicate(DateTimeParseResult pr, Match match) { + return !(match.index < pr.getStart() + pr.getLength() && pr.getStart() < match.index + match.length); + } + + public DateTimeParseResult setParseResult(DateTimeParseResult slot, boolean hasMod) { + SortedMap slotValue = dateTimeResolution(slot); + // Change the type at last for the after or before modes + String type = String.format("%s.%s", parserName, determineDateTimeType(slot.getType(), hasMod)); + + slot.setValue(slotValue); + slot.setType(type); + + return slot; + } + + public DateTimeParseResult setInclusivePeriodEnd(DateTimeParseResult slot) { + String currentType = parserName + "." + Constants.SYS_DATETIME_DATEPERIOD; + if (slot.getType().equals(currentType)) { + Stream timexStream = Arrays.asList(slot.getTimexStr().split(",|\\(|\\)")).stream(); + String[] timexComponents = timexStream.filter(str -> !str.isEmpty()).collect(Collectors.toList()).toArray(new String[0]); + + // Only handle DatePeriod like "(StartDate,EndDate,Duration)" + if (timexComponents.length == 3) { + TreeMap value = (TreeMap)slot.getValue(); + String altTimex = ""; + + if (value != null && value.containsKey(ResolutionKey.ValueSet)) { + if (value.get(ResolutionKey.ValueSet) instanceof List) { + List> valueSet = (List>)value.get(ResolutionKey.ValueSet); + if (!value.isEmpty()) { + + for (HashMap values : valueSet) { + // This is only a sanity check, as here we only handle DatePeriod like "(StartDate,EndDate,Duration)" + if (values.containsKey(DateTimeResolutionKey.START) && + values.containsKey(DateTimeResolutionKey.END) && + values.containsKey(DateTimeResolutionKey.Timex)) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDateTime startDate = LocalDate.parse(values.get(DateTimeResolutionKey.START), formatter).atStartOfDay(); + LocalDateTime endDate = LocalDate.parse(values.get(DateTimeResolutionKey.END), formatter).atStartOfDay(); + String durationStr = timexComponents[2]; + DatePeriodTimexType datePeriodTimexType = TimexUtility.getDatePeriodTimexType(durationStr); + + endDate = TimexUtility.offsetDateObject(endDate, 1, datePeriodTimexType); + values.put(DateTimeResolutionKey.END, DateTimeFormatUtil.luisDate(endDate)); + values.put(DateTimeResolutionKey.Timex, generateEndInclusiveTimex(slot.getTimexStr(), datePeriodTimexType, startDate, endDate)); + + if (StringUtility.isNullOrEmpty(altTimex)) { + altTimex = values.get(DateTimeResolutionKey.Timex); + } + } + } + } + } + } + + slot.setValue(value); + slot.setTimexStr(altTimex); + } + } + return slot; + } + + public String generateEndInclusiveTimex(String originalTimex, DatePeriodTimexType datePeriodTimexType, LocalDateTime startDate, LocalDateTime endDate) { + String timexEndInclusive = TimexUtility.generateDatePeriodTimex(startDate, endDate, datePeriodTimexType); + + // Sometimes the original timex contains fuzzy part like "XXXX-05-31" + // The fuzzy part needs to stay the same in the new end-inclusive timex + if (originalTimex.contains(Character.toString(Constants.TimexFuzzy)) && originalTimex.length() == timexEndInclusive.length()) { + char[] timexCharSet = new char[timexEndInclusive.length()]; + + for (int i = 0; i < originalTimex.length(); i++) { + if (originalTimex.charAt(i) != Constants.TimexFuzzy) { + timexCharSet[i] = timexEndInclusive.charAt(i); + } else { + timexCharSet[i] = Constants.TimexFuzzy; + } + } + + timexEndInclusive = new String(timexCharSet); + } + + return timexEndInclusive; + } + + public String determineDateTimeType(String type, boolean hasMod) { + if (config.getOptions().match(DateTimeOptions.SplitDateAndTime)) { + if (type.equals(Constants.SYS_DATETIME_DATETIME)) { + return Constants.SYS_DATETIME_TIME; + } + } else { + if (hasMod) { + if (type.equals(Constants.SYS_DATETIME_DATE)) { + return Constants.SYS_DATETIME_DATEPERIOD; + } + + if (type.equals(Constants.SYS_DATETIME_TIME)) { + return Constants.SYS_DATETIME_TIMEPERIOD; + } + + if (type.equals(Constants.SYS_DATETIME_DATETIME)) { + return Constants.SYS_DATETIME_DATETIMEPERIOD; + } + } + } + + return type; + } + + public String determineSourceEntityType(String sourceType, String newType, boolean hasMod) { + if (!hasMod) { + return null; + } + + if (!newType.equals(sourceType)) { + return Constants.SYS_DATETIME_DATETIMEPOINT; + } + + if (newType.equals(Constants.SYS_DATETIME_DATEPERIOD)) { + return Constants.SYS_DATETIME_DATETIMEPERIOD; + } + + return null; + } + + public List dateTimeResolutionForSplit(DateTimeParseResult slot) { + List results = new ArrayList<>(); + if (((DateTimeResolutionResult)slot.getValue()).getSubDateTimeEntities() != null) { + List subEntities = ((DateTimeResolutionResult)slot.getValue()).getSubDateTimeEntities(); + for (Object subEntity : subEntities) { + DateTimeParseResult result = (DateTimeParseResult)subEntity; + result.setStart(result.getStart() + slot.getStart()); + results.addAll(dateTimeResolutionForSplit(result)); + } + } else { + slot.setValue(dateTimeResolution(slot)); + slot.setType(String.format("%s.%s",parserName, determineDateTimeType(slot.getType(), false))); + + results.add(slot); + } + + return results; + } + + public SortedMap dateTimeResolution(DateTimeParseResult slot) { + if (slot == null) { + return null; + } + + List> resolutions = new ArrayList<>(); + LinkedHashMap res = new LinkedHashMap<>(); + + DateTimeResolutionResult val = (DateTimeResolutionResult)slot.getValue(); + if (val == null) { + return null; + } + + boolean islunar = val.getIsLunar() != null ? val.getIsLunar() : false; + String mod = val.getMod(); + + String list = null; + + // Resolve dates list for date periods + if (slot.getType().equals(Constants.SYS_DATETIME_DATEPERIOD) && val.getList() != null) { + list = String.join(",", val.getList().stream().map(o -> DateTimeFormatUtil.luisDate((LocalDateTime)o)).collect(Collectors.toList())); + } + + // With modifier, output Type might not be the same with type in resolution comments + // For example, if the resolution type is "date", with modifier the output type should be "daterange" + String typeOutput = determineDateTimeType(slot.getType(), !StringUtility.isNullOrEmpty(mod)); + String sourceEntity = determineSourceEntityType(slot.getType(), typeOutput, val.getHasRangeChangingMod()); + String comment = val.getComment(); + + String type = slot.getType(); + String timex = slot.getTimexStr(); + + // The following should be added to res first, since ResolveAmPm requires these fields. + addResolutionFields(res, DateTimeResolutionKey.Timex, timex); + addResolutionFields(res, Constants.Comment, comment); + addResolutionFields(res, DateTimeResolutionKey.Mod, mod); + addResolutionFields(res, ResolutionKey.Type, typeOutput); + addResolutionFields(res, DateTimeResolutionKey.IsLunar, islunar ? Boolean.toString(islunar) : ""); + + boolean hasTimeZone = false; + + // For standalone timezone entity recognition, we generate TimeZoneResolution for each entity we extracted. + // We also merge time entity with timezone entity and add the information in TimeZoneResolution to every DateTime resolutions. + if (val.getTimeZoneResolution() != null) { + if (slot.getType().equals(Constants.SYS_DATETIME_TIMEZONE)) { + // single timezone + Map resolutionField = new LinkedHashMap<>(); + resolutionField.put(ResolutionKey.Value, val.getTimeZoneResolution().getValue()); + resolutionField.put(Constants.UtcOffsetMinsKey, val.getTimeZoneResolution().getUtcOffsetMins().toString()); + + addResolutionFields(res, Constants.ResolveTimeZone, resolutionField); + } else { + // timezone as clarification of datetime + hasTimeZone = true; + addResolutionFields(res, Constants.TimeZone, val.getTimeZoneResolution().getValue()); + addResolutionFields(res, Constants.TimeZoneText, val.getTimeZoneResolution().getTimeZoneText()); + addResolutionFields(res, Constants.UtcOffsetMinsKey, val.getTimeZoneResolution().getUtcOffsetMins().toString()); + } + } + + LinkedHashMap pastResolutionStr = new LinkedHashMap<>(); + if (((DateTimeResolutionResult)slot.getValue()).getPastResolution() != null) { + pastResolutionStr.putAll(((DateTimeResolutionResult)slot.getValue()).getPastResolution()); + } + + Map futureResolutionStr = ((DateTimeResolutionResult)slot.getValue()).getFutureResolution(); + + if (typeOutput.equals(Constants.SYS_DATETIME_DATETIMEALT) && pastResolutionStr.size() > 0) { + typeOutput = determineResolutionDateTimeType(pastResolutionStr); + } + + Map resolutionPast = generateResolution(type, pastResolutionStr, mod); + Map resolutionFuture = generateResolution(type, futureResolutionStr, mod); + + // If past and future are same, keep only one + if (resolutionFuture.equals(resolutionPast)) { + if (resolutionPast.size() > 0) { + addResolutionFields(res, Constants.Resolve, resolutionPast); + } + } else { + if (resolutionPast.size() > 0) { + addResolutionFields(res, Constants.ResolveToPast, resolutionPast); + } + + if (resolutionFuture.size() > 0) { + addResolutionFields(res, Constants.ResolveToFuture, resolutionFuture); + } + } + + // If 'ampm', double our resolution accordingly + if (!StringUtility.isNullOrEmpty(comment) && comment.equals(Constants.Comment_AmPm)) { + if (res.containsKey(Constants.Resolve)) { + resolveAmPm(res, Constants.Resolve); + } else { + resolveAmPm(res, Constants.ResolveToPast); + resolveAmPm(res, Constants.ResolveToFuture); + } + } + + // If WeekOf and in CalendarMode, modify the past part of our resolution + if (config.getOptions().match(DateTimeOptions.CalendarMode) && + !StringUtility.isNullOrEmpty(comment) && comment.equals(Constants.Comment_WeekOf)) { + resolveWeekOf(res, Constants.ResolveToPast); + } + + if (comment != null && !comment.isEmpty() && TimexUtility.hasDoubleTimex(comment)) { + res = TimexUtility.processDoubleTimex(res, Constants.ResolveToFuture, Constants.ResolveToPast, timex); + } + + for (Map.Entry p : res.entrySet()) { + if (p.getValue() instanceof Map) { + Map value = new LinkedHashMap<>(); + + addResolutionFields(value, DateTimeResolutionKey.Timex, timex); + addResolutionFields(value, DateTimeResolutionKey.Mod, mod); + addResolutionFields(value, ResolutionKey.Type, typeOutput); + addResolutionFields(value, DateTimeResolutionKey.IsLunar, islunar ? Boolean.toString(islunar) : ""); + addResolutionFields(value, DateTimeResolutionKey.List, list); + addResolutionFields(value, DateTimeResolutionKey.SourceEntity, sourceEntity); + + if (hasTimeZone) { + addResolutionFields(value, Constants.TimeZone, val.getTimeZoneResolution().getValue()); + addResolutionFields(value, Constants.TimeZoneText, val.getTimeZoneResolution().getTimeZoneText()); + addResolutionFields(value, Constants.UtcOffsetMinsKey, val.getTimeZoneResolution().getUtcOffsetMins().toString()); + } + + for (Map.Entry q : ((Map)p.getValue()).entrySet()) { + value.put(q.getKey(), q.getValue()); + } + + resolutions.add(value); + } + } + + if (resolutionPast.size() == 0 && resolutionFuture.size() == 0 && val.getTimeZoneResolution() == null) { + Map notResolved = new LinkedHashMap<>(); + notResolved.put(DateTimeResolutionKey.Timex, timex); + notResolved.put(ResolutionKey.Type, typeOutput); + notResolved.put(ResolutionKey.Value, "not resolved"); + + resolutions.add(notResolved); + } + + SortedMap result = new TreeMap<>(); + result.put(ResolutionKey.ValueSet, resolutions); + + return result; + } + + private String combineMod(String originalMod, String newMod) { + String combinedMod = newMod; + if (originalMod != null && originalMod != "") { + combinedMod = newMod + "-" + originalMod; + } + return combinedMod; + } + + private String determineResolutionDateTimeType(LinkedHashMap pastResolutionStr) { + switch (pastResolutionStr.keySet().stream().findFirst().get()) { + case TimeTypeConstants.START_DATE: + return Constants.SYS_DATETIME_DATEPERIOD; + case TimeTypeConstants.START_DATETIME: + return Constants.SYS_DATETIME_DATETIMEPERIOD; + case TimeTypeConstants.START_TIME: + return Constants.SYS_DATETIME_TIMEPERIOD; + default: + return pastResolutionStr.keySet().stream().findFirst().get().toLowerCase(); + } + } + + private void addResolutionFields(Map dic, String key, Object value) { + if (value != null) { + dic.put(key, value); + } + } + + private void addResolutionFields(Map dic, String key, String value) { + if (!StringUtility.isNullOrEmpty(value)) { + dic.put(key, value); + } + } + + private void resolveAmPm(Map resolutionDic, String keyName) { + if (resolutionDic.containsKey(keyName)) { + Map resolution = (Map)resolutionDic.get(keyName); + + Map resolutionPm = new LinkedHashMap<>(); + + if (!resolutionDic.containsKey(DateTimeResolutionKey.Timex)) { + return; + } + + String timex = (String)resolutionDic.get(DateTimeResolutionKey.Timex); + timex = timex != null ? timex : ""; + + resolutionDic.remove(keyName); + resolutionDic.put(keyName + "Am", resolution); + + switch ((String)resolutionDic.get(ResolutionKey.Type)) { + case Constants.SYS_DATETIME_TIME: + resolutionPm.put(ResolutionKey.Value, DateTimeFormatUtil.toPm(resolution.get(ResolutionKey.Value))); + resolutionPm.put(DateTimeResolutionKey.Timex, DateTimeFormatUtil.toPm(timex)); + break; + case Constants.SYS_DATETIME_DATETIME: + String[] splited = resolution.get(ResolutionKey.Value).split(" "); + resolutionPm.put(ResolutionKey.Value, splited[0] + " " + DateTimeFormatUtil.toPm(splited[1])); + resolutionPm.put(DateTimeResolutionKey.Timex, DateTimeFormatUtil.allStringToPm(timex)); + break; + case Constants.SYS_DATETIME_TIMEPERIOD: + if (resolution.containsKey(DateTimeResolutionKey.START)) { + resolutionPm.put(DateTimeResolutionKey.START, DateTimeFormatUtil.toPm(resolution.get(DateTimeResolutionKey.START))); + } + + if (resolution.containsKey(DateTimeResolutionKey.END)) { + resolutionPm.put(DateTimeResolutionKey.END, DateTimeFormatUtil.toPm(resolution.get(DateTimeResolutionKey.END))); + } + + resolutionPm.put(DateTimeResolutionKey.Timex, DateTimeFormatUtil.allStringToPm(timex)); + break; + case Constants.SYS_DATETIME_DATETIMEPERIOD: + if (resolution.containsKey(DateTimeResolutionKey.START)) { + LocalDateTime start = LocalDateTime.parse(resolution.get(DateTimeResolutionKey.START), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + start = start.getHour() == Constants.HalfDayHourCount ? start.minusHours(Constants.HalfDayHourCount) : start.plusHours(Constants.HalfDayHourCount); + + resolutionPm.put(DateTimeResolutionKey.START, DateTimeFormatUtil.formatDateTime(start)); + } + + if (resolution.containsKey(DateTimeResolutionKey.END)) { + LocalDateTime end = LocalDateTime.parse(resolution.get(DateTimeResolutionKey.END), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + end = end.getHour() == Constants.HalfDayHourCount ? end.minusHours(Constants.HalfDayHourCount) : end.plusHours(Constants.HalfDayHourCount); + + resolutionPm.put(DateTimeResolutionKey.END, DateTimeFormatUtil.formatDateTime(end)); + } + + resolutionPm.put(DateTimeResolutionKey.Timex, DateTimeFormatUtil.allStringToPm(timex)); + break; + default: + break; + } + resolutionDic.put(keyName + "Pm", resolutionPm); + } + } + + private void resolveWeekOf(Map resolutionDic, String keyName) { + if (resolutionDic.containsKey(keyName)) { + Map resolution = (Map)resolutionDic.get(keyName); + + LocalDateTime monday = DateUtil.tryParse(resolution.get(DateTimeResolutionKey.START)); + resolution.put(DateTimeResolutionKey.Timex, TimexUtility.generateWeekTimex(monday)); + + resolutionDic.put(keyName, resolution); + } + } + + private Map generateResolution(String type, Map resolutionDic, String mod) { + Map res = new LinkedHashMap<>(); + + if (type.equals(Constants.SYS_DATETIME_DATETIME)) { + addSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.DATETIME, mod, res); + } else if (type.equals(Constants.SYS_DATETIME_TIME)) { + addSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.TIME, mod, res); + } else if (type.equals(Constants.SYS_DATETIME_DATE)) { + addSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.DATE, mod, res); + } else if (type.equals(Constants.SYS_DATETIME_DURATION)) { + if (resolutionDic.containsKey(TimeTypeConstants.DURATION)) { + res.put(ResolutionKey.Value, resolutionDic.get(TimeTypeConstants.DURATION)); + } + } else if (type.equals(Constants.SYS_DATETIME_TIMEPERIOD)) { + addPeriodToResolution(resolutionDic, TimeTypeConstants.START_TIME, TimeTypeConstants.END_TIME, mod, res); + } else if (type.equals(Constants.SYS_DATETIME_DATEPERIOD)) { + addPeriodToResolution(resolutionDic, TimeTypeConstants.START_DATE, TimeTypeConstants.END_DATE, mod, res); + } else if (type.equals(Constants.SYS_DATETIME_DATETIMEPERIOD)) { + addPeriodToResolution(resolutionDic, TimeTypeConstants.START_DATETIME, TimeTypeConstants.END_DATETIME, mod, res); + } else if (type.equals(Constants.SYS_DATETIME_DATETIMEALT)) { + // for a period + if (resolutionDic.size() > 2) { + addAltPeriodToResolution(resolutionDic, mod, res); + } else { + // for a datetime point + addAltSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.DATETIMEALT, mod, res); + } + } + + return res; + } + + public void addAltPeriodToResolution(Map resolutionDic, String mod, Map res) { + if (resolutionDic.containsKey(TimeTypeConstants.START_DATETIME) && resolutionDic.containsKey(TimeTypeConstants.END_DATETIME)) { + addPeriodToResolution(resolutionDic, TimeTypeConstants.START_DATETIME, TimeTypeConstants.END_DATETIME, mod, res); + } else if (resolutionDic.containsKey(TimeTypeConstants.START_DATE) && resolutionDic.containsKey(TimeTypeConstants.END_DATE)) { + addPeriodToResolution(resolutionDic, TimeTypeConstants.START_DATE, TimeTypeConstants.END_DATE, mod, res); + } else if (resolutionDic.containsKey(TimeTypeConstants.START_TIME) && resolutionDic.containsKey(TimeTypeConstants.END_TIME)) { + addPeriodToResolution(resolutionDic, TimeTypeConstants.START_TIME, TimeTypeConstants.END_TIME, mod, res); + } + } + + public void addAltSingleDateTimeToResolution(Map resolutionDic, String type, String mod, Map res) { + if (resolutionDic.containsKey(TimeTypeConstants.DATE)) { + addSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.DATE, mod, res); + } else if (resolutionDic.containsKey(TimeTypeConstants.DATETIME)) { + addSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.DATETIME, mod, res); + } else if (resolutionDic.containsKey(TimeTypeConstants.TIME)) { + addSingleDateTimeToResolution(resolutionDic, TimeTypeConstants.TIME, mod, res); + } + } + + public void addSingleDateTimeToResolution(Map resolutionDic, String type, String mod, Map res) { + // If an "invalid" Date or DateTime is extracted, it should not have an assigned resolution. + // Only valid entities should pass this condition. + if (resolutionDic.containsKey(type) && !resolutionDic.get(type).startsWith(dateMinString)) { + if (!StringUtility.isNullOrEmpty(mod)) { + if (mod.equals(Constants.BEFORE_MOD)) { + res.put(DateTimeResolutionKey.END, resolutionDic.get(type)); + return; + } + + if (mod.equals(Constants.AFTER_MOD)) { + res.put(DateTimeResolutionKey.START, resolutionDic.get(type)); + return; + } + + if (mod.equals(Constants.SINCE_MOD)) { + res.put(DateTimeResolutionKey.START, resolutionDic.get(type)); + return; + } + + if (mod.equals(Constants.UNTIL_MOD)) { + res.put(DateTimeResolutionKey.END, resolutionDic.get(type)); + return; + } + } + + res.put(ResolutionKey.Value, resolutionDic.get(type)); + } + } + + public void addPeriodToResolution(Map resolutionDic, String startType, String endType, String mod, Map res) { + String start = ""; + String end = ""; + + if (resolutionDic.containsKey(startType)) { + if (resolutionDic.get(startType).startsWith(dateMinString)) { + return; + } + start = resolutionDic.get(startType); + } + + if (resolutionDic.containsKey(endType)) { + if (resolutionDic.get(endType).startsWith(dateMinString)) { + return; + } + end = resolutionDic.get(endType); + } + + if (!StringUtility.isNullOrEmpty(mod)) { + // For the 'before' mod + // 1. Cases like "Before December", the start of the period should be the end of the new period, not the start + // 2. Cases like "More than 3 days before today", the date point should be the end of the new period + if (mod.startsWith(Constants.BEFORE_MOD)) { + if (!StringUtility.isNullOrEmpty(start) && !StringUtility.isNullOrEmpty(end) && !mod.endsWith(Constants.LATE_MOD)) { + res.put(DateTimeResolutionKey.END, start); + } else { + res.put(DateTimeResolutionKey.END, end); + } + + return; + } + + // For the 'after' mod + // 1. Cases like "After January", the end of the period should be the start of the new period, not the end + // 2. Cases like "More than 3 days after today", the date point should be the start of the new period + if (mod.startsWith(Constants.AFTER_MOD)) { + if (!StringUtility.isNullOrEmpty(start) && !StringUtility.isNullOrEmpty(end) && !mod.endsWith(Constants.EARLY_MOD)) { + res.put(DateTimeResolutionKey.START, end); + } else { + res.put(DateTimeResolutionKey.START, start); + } + + return; + } + + // For the 'since' mod, the start of the period should be the start of the new period, not the end + if (mod.equals(Constants.SINCE_MOD)) { + res.put(DateTimeResolutionKey.START, start); + return; + } + + // For the 'until' mod, the end of the period should be the end of the new period, not the start + if (mod.equals(Constants.UNTIL_MOD)) { + res.put(DateTimeResolutionKey.END, end); + return; + } + } + + if (!StringUtility.isNullOrEmpty(start) && !StringUtility.isNullOrEmpty(end)) { + res.put(DateTimeResolutionKey.START, start); + res.put(DateTimeResolutionKey.END, end); + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseSetParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseSetParser.java new file mode 100644 index 000000000..220773230 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseSetParser.java @@ -0,0 +1,251 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.config.ISetParserConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.MatchedTimexResult; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; + +public class BaseSetParser implements IDateTimeParser { + @Override + public String getParserName() { + return Constants.SYS_DATETIME_SET; + } + + private ISetParserConfiguration config; + + public BaseSetParser(ISetParserConfiguration configuration) { + this.config = configuration; + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + DateTimeResolutionResult value = null; + + if (er.getType().equals(getParserName())) { + + DateTimeResolutionResult innerResult = parseEachUnit(er.getText()); + if (!innerResult.getSuccess()) { + innerResult = parseEachDuration(er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parserTimeEveryday(er.getText(), reference); + } + + // NOTE: Please do not change the order of following function + // datetimeperiod>dateperiod>timeperiod>datetime>date>time + if (!innerResult.getSuccess()) { + innerResult = parseEach(config.getDateTimePeriodExtractor(), config.getDateTimePeriodParser(), er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseEach(config.getDatePeriodExtractor(), config.getDatePeriodParser(), er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseEach(config.getTimePeriodExtractor(), config.getTimePeriodParser(), er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseEach(config.getDateTimeExtractor(), config.getDateTimeParser(), er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseEach(config.getDateExtractor(), config.getDateParser(), er.getText(), reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseEach(config.getTimeExtractor(), config.getTimeParser(), er.getText(), reference); + } + + if (innerResult.getSuccess()) { + HashMap futureMap = new HashMap<>(); + futureMap.put(TimeTypeConstants.SET, innerResult.getFutureValue().toString()); + innerResult.setFutureResolution(futureMap); + + HashMap pastMap = new HashMap<>(); + pastMap.put(TimeTypeConstants.SET, innerResult.getPastValue().toString()); + innerResult.setPastResolution(pastMap); + + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : value.getTimex() + ); + + return ret; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public List filterResults(String query, List candidateResults) { + throw new UnsupportedOperationException(); + } + + private DateTimeResolutionResult parseEachDuration(String text, LocalDateTime refDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + List ers = this.config.getDurationExtractor().extract(text, refDate); + if (ers.size() != 1 || !StringUtility.isNullOrWhiteSpace(text.substring(ers.get(0).getStart() + ers.get(0).getLength()))) { + return ret; + } + + String beforeStr = text.substring(0, ers.get(0).getStart()); + Matcher regexMatch = this.config.getEachPrefixRegex().matcher(beforeStr); + if (regexMatch.find()) { + DateTimeParseResult pr = this.config.getDurationParser().parse(ers.get(0), LocalDateTime.now()); + ret.setTimex(pr.getTimexStr()); + ret.setFutureValue("Set: " + pr.getTimexStr()); + ret.setPastValue("Set: " + pr.getTimexStr()); + ret.setSuccess(true); + return ret; + } + + return ret; + } + + private DateTimeResolutionResult parseEachUnit(String text) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + // handle "daily", "weekly" + Optional matched = Arrays.stream(RegExpUtility.getMatches(this.config.getPeriodicRegex(), text)).findFirst(); + if (matched.isPresent()) { + + MatchedTimexResult result = this.config.getMatchedDailyTimex(text); + if (!result.getResult()) { + return ret; + } + + ret.setTimex(result.getTimex()); + ret.setFutureValue("Set: " + ret.getTimex()); + ret.setPastValue("Set: " + ret.getTimex()); + ret.setSuccess(true); + + return ret; + } + + // Handle "each month" + ConditionalMatch exactMatch = RegexExtension.matchExact(this.config.getEachUnitRegex(), text, true); + if (exactMatch.getSuccess()) { + + String sourceUnit = exactMatch.getMatch().get().getGroup("unit").value; + if (!StringUtility.isNullOrEmpty(sourceUnit) && this.config.getUnitMap().containsKey(sourceUnit)) { + + MatchedTimexResult result = this.config.getMatchedUnitTimex(sourceUnit); + if (!result.getResult()) { + return ret; + } + + // Handle "every other month" + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getEachUnitRegex(), text)).findFirst(); + + if (exactMatch.getMatch().get().getGroup("other").value != "") { + result.setTimex(result.getTimex().replace("1", "2")); + } + + ret.setTimex(result.getTimex()); + ret.setFutureValue("Set: " + ret.getTimex()); + ret.setPastValue("Set: " + ret.getTimex()); + ret.setSuccess(true); + + return ret; + } + } + + return ret; + } + + private DateTimeResolutionResult parserTimeEveryday(String text, LocalDateTime refDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + List ers = this.config.getTimeExtractor().extract(text, refDate); + if (ers.size() != 1) { + return ret; + } + + String afterStr = text.replace(ers.get(0).getText(), ""); + Matcher match = this.config.getEachDayRegex().matcher(afterStr); + if (match.find()) { + DateTimeParseResult pr = this.config.getTimeParser().parse(ers.get(0), LocalDateTime.now()); + ret.setTimex(pr.getTimexStr()); + ret.setFutureValue("Set: " + ret.getTimex()); + ret.setPastValue("Set: " + ret.getTimex()); + ret.setSuccess(true); + + return ret; + } + + return ret; + } + + private DateTimeResolutionResult parseEach(IDateTimeExtractor extractor, IDateTimeParser parser, String text, LocalDateTime refDate) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + List ers = null; + + // remove key words of set type from text + boolean success = false; + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getSetEachRegex(), text)).findFirst(); + if (match.isPresent()) { + + StringBuilder sb = new StringBuilder(text); + String trimmedText = sb.delete(match.get().index, match.get().index + match.get().length).toString(); + ers = extractor.extract(trimmedText, refDate); + if (ers.size() == 1 && ers.get(0).getLength() == trimmedText.length()) { + + success = true; + } + } + + // remove suffix 's' and "on" if existed and re-try + match = Arrays.stream(RegExpUtility.getMatches(this.config.getSetWeekDayRegex(), text)).findFirst(); + if (match.isPresent()) { + + StringBuilder sb = new StringBuilder(text); + String trimmedText = sb.delete(match.get().index, match.get().index + match.get().length).toString(); + trimmedText = new StringBuilder(trimmedText).insert(match.get().index, match.get().getGroup("weekday").value).toString(); + ers = extractor.extract(trimmedText, refDate); + if (ers.size() == 1 && ers.get(0).getLength() == trimmedText.length()) { + + success = true; + } + } + + if (success) { + DateTimeParseResult pr = parser.parse(ers.get(0), refDate); + ret.setTimex(pr.getTimexStr()); + ret.setFutureValue("Set: " + ret.getTimex()); + ret.setPastValue("Set: " + ret.getTimex()); + ret.setSuccess(true); + + return ret; + } + + return ret; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimeParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimeParser.java new file mode 100644 index 000000000..ce61d453f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimeParser.java @@ -0,0 +1,358 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.PrefixAdjustResult; +import com.microsoft.recognizers.text.datetime.parsers.config.SuffixAdjustResult; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneUtility; +import com.microsoft.recognizers.text.utilities.IntegerUtility; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +public class BaseTimeParser implements IDateTimeParser { + + private final ITimeParserConfiguration config; + + public BaseTimeParser(ITimeParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_TIME; + } + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + + LocalDateTime referenceTime = reference; + + Object value = null; + + if (er.getType().equals(getParserName())) { + DateTimeResolutionResult innerResult; + + // Resolve timezome + if (TimeZoneUtility.shouldResolveTimeZone(er, config.getOptions())) { + Map metadata = (Map)er.getData(); + ExtractResult timezoneEr = (ExtractResult)metadata.get(Constants.SYS_DATETIME_TIMEZONE); + ParseResult timezonePr = config.getTimeZoneParser().parse(timezoneEr); + + innerResult = internalParse(er.getText().substring(0, er.getText().length() - timezoneEr.getLength()), referenceTime); + + if (timezonePr.getValue() != null) { + TimeZoneResolutionResult timeZoneResolution = ((DateTimeResolutionResult)timezonePr.getValue()).getTimeZoneResolution(); + innerResult.setTimeZoneResolution(timeZoneResolution); + } + + } else { + innerResult = internalParse(er.getText(), referenceTime); + } + + if (innerResult.getSuccess()) { + ImmutableMap.Builder futureResolution = ImmutableMap.builder(); + futureResolution.put(TimeTypeConstants.TIME, DateTimeFormatUtil.formatTime((LocalDateTime)innerResult.getFutureValue())); + + innerResult.setFutureResolution(futureResolution.build()); + + ImmutableMap.Builder pastResolution = ImmutableMap.builder(); + pastResolution.put(TimeTypeConstants.TIME, DateTimeFormatUtil.formatTime((LocalDateTime)innerResult.getPastValue())); + + innerResult.setPastResolution(pastResolution.build()); + + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex()); + + return ret; + } + + protected DateTimeResolutionResult internalParse(String text, LocalDateTime referenceTime) { + + DateTimeResolutionResult innerResult = parseBasicRegexMatch(text, referenceTime); + return innerResult; + } + + // parse basic patterns in TimeRegexList + private DateTimeResolutionResult parseBasicRegexMatch(String text, LocalDateTime referenceTime) { + + String trimmedText = text.trim().toLowerCase(); + int offset = 0; + Optional match = Arrays.stream(RegExpUtility.getMatches(config.getAtRegex(), trimmedText)).findFirst(); + if (!match.isPresent()) { + match = Arrays.stream(RegExpUtility.getMatches(config.getAtRegex(), config.getTimeTokenPrefix() + trimmedText)).findFirst(); + offset = config.getTimeTokenPrefix().length(); + } + + if (match.isPresent() && match.get().index == offset && match.get().length == trimmedText.length()) { + return match2Time(match.get(), referenceTime); + } + + // parse hour pattern, like "twenty one", "16" + // create a extract comments which content the pass-in text + Integer hour = null; + if (config.getNumbers().containsKey(text)) { + hour = config.getNumbers().get(text); + } else { + try { + hour = Integer.parseInt(text); + } catch (Exception ignored) { + hour = null; + } + } + + if (hour != null && hour >= 0 && hour <= 24) { + DateTimeResolutionResult result = new DateTimeResolutionResult(); + + if (hour == 24) { + hour = 0; + } + + if (hour <= Constants.HalfDayHourCount && hour != 0) { + result.setComment(Constants.Comment_AmPm); + } + + result.setTimex(String.format("T%02d", hour)); + LocalDateTime value = DateUtil.safeCreateFromMinValue(referenceTime.getYear(), referenceTime.getMonthValue(), referenceTime.getDayOfMonth(), hour, 0, 0); + result.setFutureValue(value); + result.setPastValue(value); + result.setSuccess(true); + return result; + } + + for (Pattern regex : config.getTimeRegexes()) { + ConditionalMatch exactMatch = RegexExtension.matchExact(regex, trimmedText, true); + + if (exactMatch.getSuccess()) { + return match2Time(exactMatch.getMatch().get(), referenceTime); + } + } + + return new DateTimeResolutionResult(); + } + + private DateTimeResolutionResult match2Time(Match match, LocalDateTime referenceTime) { + + DateTimeResolutionResult result = new DateTimeResolutionResult(); + boolean hasMin = false; + boolean hasSec = false; + boolean hasAm = false; + boolean hasPm = false; + boolean hasMid = false; + int hour = 0; + int minute = 0; + int second = 0; + int day = referenceTime.getDayOfMonth(); + int month = referenceTime.getMonthValue(); + int year = referenceTime.getYear(); + + String writtenTimeStr = match.getGroup("writtentime").value; + + if (!StringUtility.isNullOrEmpty(writtenTimeStr)) { + // get hour + String hourStr = match.getGroup("hournum").value.toLowerCase(); + hour = config.getNumbers().get(hourStr); + + // get minute + String minStr = match.getGroup("minnum").value.toLowerCase(); + String tensStr = match.getGroup("tens").value.toLowerCase(); + + if (!StringUtility.isNullOrEmpty(minStr)) { + minute = config.getNumbers().get(minStr); + if (!StringUtility.isNullOrEmpty(tensStr)) { + minute += config.getNumbers().get(tensStr); + } + hasMin = true; + } + } else if (!StringUtility.isNullOrEmpty(match.getGroup("mid").value)) { + hasMid = true; + if (!StringUtility.isNullOrEmpty(match.getGroup("midnight").value)) { + hour = 0; + minute = 0; + second = 0; + } else if (!StringUtility.isNullOrEmpty(match.getGroup("midmorning").value)) { + hour = 10; + minute = 0; + second = 0; + } else if (!StringUtility.isNullOrEmpty(match.getGroup("midafternoon").value)) { + hour = 14; + minute = 0; + second = 0; + } else if (!StringUtility.isNullOrEmpty(match.getGroup("midday").value)) { + hour = Constants.HalfDayHourCount; + minute = 0; + second = 0; + } + } else { + // get hour + String hourStr = match.getGroup(Constants.HourGroupName).value; + if (StringUtility.isNullOrEmpty(hourStr)) { + hourStr = match.getGroup("hournum").value.toLowerCase(); + if (!config.getNumbers().containsKey(hourStr)) { + return result; + } + + hour = config.getNumbers().get(hourStr); + } else { + if (!IntegerUtility.canParse(hourStr)) { + if (!config.getNumbers().containsKey(hourStr.toLowerCase())) { + return result; + } + + hour = config.getNumbers().get(hourStr.toLowerCase()); + } else { + hour = Integer.parseInt(hourStr); + } + } + + // get minute + String minStr = match.getGroup(Constants.MinuteGroupName).value.toLowerCase(); + if (StringUtility.isNullOrEmpty(minStr)) { + minStr = match.getGroup("minnum").value; + if (!StringUtility.isNullOrEmpty(minStr)) { + minute = config.getNumbers().get(minStr); + hasMin = true; + } + + String tensStr = match.getGroup("tens").value; + if (!StringUtility.isNullOrEmpty(tensStr)) { + minute += config.getNumbers().get(tensStr); + hasMin = true; + } + } else { + minute = Integer.parseInt(minStr); + hasMin = true; + } + + // get second + String secStr = match.getGroup(Constants.SecondGroupName).value.toLowerCase(); + if (!StringUtility.isNullOrEmpty(secStr)) { + second = Integer.parseInt(secStr); + hasSec = true; + } + } + + // Adjust by desc string + String descStr = match.getGroup(Constants.DescGroupName).value.toLowerCase(); + + // ampm is a special case in which at 6ampm = at 6 + if (isAmDesc(descStr, match)) { + if (hour >= Constants.HalfDayHourCount) { + hour -= Constants.HalfDayHourCount; + } + + if (!checkRegex(config.getUtilityConfiguration().getAmPmDescRegex(), descStr)) { + hasAm = true; + } + + } else if (isPmDesc(descStr, match)) { + if (hour < Constants.HalfDayHourCount) { + hour += Constants.HalfDayHourCount; + } + + hasPm = true; + } + + // adjust min by prefix + String timePrefix = match.getGroup(Constants.PrefixGroupName).value.toLowerCase(); + if (!StringUtility.isNullOrEmpty(timePrefix)) { + PrefixAdjustResult prefixResult = config.adjustByPrefix(timePrefix, hour, minute, hasMin); + hour = prefixResult.hour; + minute = prefixResult.minute; + hasMin = prefixResult.hasMin; + } + + // adjust hour by suffix + String timeSuffix = match.getGroup(Constants.SuffixGroupName).value.toLowerCase(); + if (!StringUtility.isNullOrEmpty(timeSuffix)) { + SuffixAdjustResult suffixResult = config.adjustBySuffix(timeSuffix, hour, minute, hasMin, hasAm, hasPm); + hour = suffixResult.hour; + minute = suffixResult.minute; + hasMin = suffixResult.hasMin; + hasAm = suffixResult.hasAm; + hasPm = suffixResult.hasPm; + } + + if (hour == 24) { + hour = 0; + } + + StringBuilder timex = new StringBuilder(String.format("T%02d", hour)); + + if (hasMin) { + timex.append(String.format(":%02d", minute)); + } + + if (hasSec) { + timex.append(String.format(":%02d", second)); + } + + result.setTimex(timex.toString()); + + if (hour <= Constants.HalfDayHourCount && !hasPm && !hasAm && !hasMid) { + result.setComment(Constants.Comment_AmPm); + } + + LocalDateTime resultTime = DateUtil.safeCreateFromMinValue(year, month, day, hour, minute, second); + result.setFutureValue(resultTime); + result.setPastValue(resultTime); + + result.setSuccess(true); + + return result; + } + + private boolean isAmDesc(String descStr, Match match) { + return checkRegex(config.getUtilityConfiguration().getAmDescRegex(), descStr) || + checkRegex(config.getUtilityConfiguration().getAmPmDescRegex(), descStr) || + !StringUtility.isNullOrEmpty(match.getGroup(Constants.ImplicitAmGroupName).value); + } + + private boolean isPmDesc(String descStr, Match match) { + return checkRegex(config.getUtilityConfiguration().getPmDescRegex(), descStr) || + !StringUtility.isNullOrEmpty(match.getGroup(Constants.ImplicitPmGroupName).value); + } + + private boolean checkRegex(Pattern regex, String input) { + Optional result = Arrays.stream(RegExpUtility.getMatches(regex, input)).findFirst(); + return result.isPresent(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimePeriodParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimePeriodParser.java new file mode 100644 index 000000000..c0b3fffe6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimePeriodParser.java @@ -0,0 +1,697 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.TimeTypeConstants; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneUtility; +import com.microsoft.recognizers.text.utilities.Capture; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.javatuples.Pair; + +public class BaseTimePeriodParser implements IDateTimeParser { + + private final ITimePeriodParserConfiguration config; + + private static final String parserName = Constants.SYS_DATETIME_TIMEPERIOD; //"TimePeriod"; + + public BaseTimePeriodParser(ITimePeriodParserConfiguration config) { + this.config = config; + } + + @Override + public String getParserName() { + return parserName; + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + Object value = null; + + if (er.getType().equals(getParserName())) { + + DateTimeResolutionResult innerResult; + + if (TimeZoneUtility.shouldResolveTimeZone(er, config.getOptions())) { + Map metadata = (HashMap)er.getData(); + + ExtractResult timezoneEr = (ExtractResult)metadata.get(Constants.SYS_DATETIME_TIMEZONE); + ParseResult timezonePr = config.getTimeZoneParser().parse(timezoneEr); + + innerResult = internalParse(er.getText().substring(0, er.getLength() - timezoneEr.getLength()), reference); + + if (timezonePr.getValue() != null) { + innerResult.setTimeZoneResolution(((DateTimeResolutionResult)timezonePr.getValue()).getTimeZoneResolution()); + } + } else { + innerResult = internalParse(er.getText(), reference); + } + + if (innerResult.getSuccess()) { + ImmutableMap.Builder futureResolution = ImmutableMap.builder(); + futureResolution.put( + TimeTypeConstants.START_TIME, + DateTimeFormatUtil.formatTime(((Pair)innerResult.getFutureValue()).getValue0())); + futureResolution.put( + TimeTypeConstants.END_TIME, + DateTimeFormatUtil.formatTime(((Pair)innerResult.getFutureValue()).getValue1())); + + innerResult.setFutureResolution(futureResolution.build()); + + ImmutableMap.Builder pastResolution = ImmutableMap.builder(); + pastResolution.put( + TimeTypeConstants.START_TIME, + DateTimeFormatUtil.formatTime(((Pair)innerResult.getPastValue()).getValue0())); + pastResolution.put( + TimeTypeConstants.END_TIME, + DateTimeFormatUtil.formatTime(((Pair)innerResult.getPastValue()).getValue1())); + + innerResult.setPastResolution(pastResolution.build()); + + value = innerResult; + } + } + + DateTimeParseResult ret = new DateTimeParseResult( + er.getStart(), + er.getLength(), + er.getText(), + er.getType(), + er.getData(), + value, + "", + value == null ? "" : ((DateTimeResolutionResult)value).getTimex()); + + return ret; + } + + private DateTimeResolutionResult internalParse(String text, LocalDateTime reference) { + DateTimeResolutionResult innerResult = parseSimpleCases(text, reference); + + if (!innerResult.getSuccess()) { + innerResult = mergeTwoTimePoints(text, reference); + } + + if (!innerResult.getSuccess()) { + innerResult = parseTimeOfDay(text, reference); + } + + return innerResult; + } + + // Cases like "from 3 to 5am" or "between 3:30 and 5" are parsed here + private DateTimeResolutionResult parseSimpleCases(String text, LocalDateTime referenceTime) { + // Cases like "from 3 to 5pm" or "between 4 and 6am", time point is pure number without colon + DateTimeResolutionResult ret = parsePureNumCases(text, referenceTime); + + if (!ret.getSuccess()) { + // Cases like "from 3:30 to 5" or "netween 3:30am to 6pm", at least one of the time point contains colon + ret = parseSpecificTimeCases(text, referenceTime); + } + + return ret; + } + + // Cases like "from 3 to 5pm" or "between 4 and 6am", time point is pure number without colon + private DateTimeResolutionResult parsePureNumCases(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + int year = referenceTime.getYear(); + int month = referenceTime.getMonthValue(); + int day = referenceTime.getDayOfMonth(); + String trimmedText = text.trim().toLowerCase(); + + ConditionalMatch match = RegexExtension.matchBegin(this.config.getPureNumberFromToRegex(), trimmedText, true); + + if (!match.getSuccess()) { + match = RegexExtension.matchBegin(this.config.getPureNumberBetweenAndRegex(), trimmedText, true); + } + + if (match.getSuccess()) { + // this "from .. to .." pattern is valid if followed by a Date OR Constants.PmGroupName + boolean isValid = false; + + // get hours + MatchGroup hourGroup = match.getMatch().get().getGroup(Constants.HourGroupName); + String hourStr = hourGroup.captures[0].value; + int afterHourIndex = hourGroup.captures[0].index + hourGroup.captures[0].length; + + // hard to integrate this part into the regex + if (afterHourIndex == trimmedText.length() || !trimmedText.substring(afterHourIndex).trim().startsWith(":")) { + + int beginHour; + if (!this.config.getNumbers().containsKey(hourStr)) { + beginHour = Integer.parseInt(hourStr); + } else { + beginHour = this.config.getNumbers().get(hourStr); + } + + hourStr = hourGroup.captures[1].value; + afterHourIndex = hourGroup.captures[1].index + hourGroup.captures[1].length; + + if (afterHourIndex == trimmedText.length() || !trimmedText.substring(afterHourIndex).trim().startsWith(":")) { + int endHour; + if (!this.config.getNumbers().containsKey(hourStr)) { + endHour = Integer.parseInt(hourStr); + } else { + endHour = this.config.getNumbers().get(hourStr); + } + + // parse Constants.PmGroupName + String leftDesc = match.getMatch().get().getGroup("leftDesc").value; + String rightDesc = match.getMatch().get().getGroup("rightDesc").value; + String pmStr = match.getMatch().get().getGroup(Constants.PmGroupName).value; + String amStr = match.getMatch().get().getGroup(Constants.AmGroupName).value; + String descStr = match.getMatch().get().getGroup(Constants.DescGroupName).value; + + // The "ampm" only occurs in time, we don't have to consider it here + if (StringUtility.isNullOrEmpty(leftDesc)) { + + boolean rightAmValid = !StringUtility.isNullOrEmpty(rightDesc) && + Arrays.stream(RegExpUtility.getMatches(config.getUtilityConfiguration().getAmDescRegex(), rightDesc.toLowerCase())).findFirst().isPresent(); + boolean rightPmValid = !StringUtility.isNullOrEmpty(rightDesc) && + Arrays.stream(RegExpUtility.getMatches(config.getUtilityConfiguration().getPmDescRegex(), rightDesc.toLowerCase())).findFirst().isPresent(); + + if (!StringUtility.isNullOrEmpty(amStr) || rightAmValid) { + if (endHour >= Constants.HalfDayHourCount) { + endHour -= Constants.HalfDayHourCount; + } + + if (beginHour >= Constants.HalfDayHourCount && beginHour - Constants.HalfDayHourCount < endHour) { + beginHour -= Constants.HalfDayHourCount; + } + + // Resolve case like "11 to 3am" + if (beginHour < Constants.HalfDayHourCount && beginHour > endHour) { + beginHour += Constants.HalfDayHourCount; + } + + isValid = true; + + } else if (!StringUtility.isNullOrEmpty(pmStr) || rightPmValid) { + + if (endHour < Constants.HalfDayHourCount) { + endHour += Constants.HalfDayHourCount; + } + + // Resolve case like "11 to 3pm" + if (beginHour + Constants.HalfDayHourCount < endHour) { + beginHour += Constants.HalfDayHourCount; + } + + isValid = true; + + } + } + + if (isValid) { + String beginStr = String.format("T%02d", beginHour); + String endStr = String.format("T%02d", endHour); + + if (endHour >= beginHour) { + ret.setTimex(String.format("(%s,%s,PT%sH)", beginStr, endStr, (endHour - beginHour))); + } else { + ret.setTimex(String.format("(%s,%s,PT%sH)", beginStr, endStr, (endHour - beginHour + 24))); + } + + // Try to get the timezone resolution + List timeErs = config.getTimeExtractor().extract(trimmedText); + for (ExtractResult er : timeErs) { + DateTimeParseResult pr = config.getTimeParser().parse(er, referenceTime); + if (((DateTimeResolutionResult)pr.getValue()).getTimeZoneResolution() != null) { + ret.setTimeZoneResolution(((DateTimeResolutionResult)pr.getValue()).getTimeZoneResolution()); + break; + } + } + + ret.setFutureValue( + new Pair(DateUtil.safeCreateFromMinValue(year, month, day, beginHour, 0, 0), + DateUtil.safeCreateFromMinValue(year, month, day, endHour, 0, 0))); + ret.setPastValue( + new Pair(DateUtil.safeCreateFromMinValue(year, month, day, beginHour, 0, 0), + DateUtil.safeCreateFromMinValue(year, month, day, endHour, 0, 0))); + + ret.setSuccess(true); + } + } + } + } + + return ret; + } + + // Cases like "from 3:30 to 5" or "between 3:30am to 6pm", at least one of the time point contains colon + private DateTimeResolutionResult parseSpecificTimeCases(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + int year = referenceTime.getYear(); + int month = referenceTime.getMonthValue(); + int day = referenceTime.getDayOfMonth(); + + // Handle cases like "from 4:30 to 5" + ConditionalMatch match = RegexExtension.matchExact(config.getSpecificTimeFromToRegex(), text, true); + + if (!match.getSuccess()) { + // Handle cases like "between 5:10 and 7" + match = RegexExtension.matchExact(config.getSpecificTimeBetweenAndRegex(), text, true); + } + + if (match.getSuccess()) { + // Cases like "half past seven" are not handled here + if (!match.getMatch().get().getGroup(Constants.PrefixGroupName).value.equals("")) { + return ret; + } + + // Cases like "4" is different with "4:00" as the Timex is different "T04H" vs "T04H00M" + // Uses this invalidFlag to differentiate + int beginHour; + int beginMinute = Constants.InvalidMinute; + int beginSecond = Constants.InvalidSecond; + int endHour; + int endMinute = Constants.InvalidMinute; + int endSecond = Constants.InvalidSecond; + + // Get time1 and time2 + MatchGroup hourGroup = match.getMatch().get().getGroup(Constants.HourGroupName); + + String hourStr = hourGroup.captures[0].value; + + if (config.getNumbers().containsKey(hourStr)) { + beginHour = config.getNumbers().get(hourStr); + } else { + beginHour = Integer.parseInt(hourStr); + } + + + hourStr = hourGroup.captures[1].value; + + if (config.getNumbers().containsKey(hourStr)) { + endHour = config.getNumbers().get(hourStr); + } else { + endHour = Integer.parseInt(hourStr); + } + + int time1StartIndex = match.getMatch().get().getGroup("time1").index; + int time1EndIndex = time1StartIndex + match.getMatch().get().getGroup("time1").length; + int time2StartIndex = match.getMatch().get().getGroup("time2").index; + int time2EndIndex = time2StartIndex + match.getMatch().get().getGroup("time2").length; + + // Get beginMinute (if exists) and endMinute (if exists) + for (int i = 0; i < match.getMatch().get().getGroup(Constants.MinuteGroupName).captures.length; i++) { + Capture minuteCapture = match.getMatch().get().getGroup(Constants.MinuteGroupName).captures[i]; + if (minuteCapture.index >= time1StartIndex && (minuteCapture.index + minuteCapture.length) <= time1EndIndex) { + beginMinute = Integer.parseInt(minuteCapture.value); + } else if (minuteCapture.index >= time2StartIndex && (minuteCapture.index + minuteCapture.length) <= time2EndIndex) { + endMinute = Integer.parseInt(minuteCapture.value); + } + } + + // Get beginSecond (if exists) and endSecond (if exists) + for (int i = 0; i < match.getMatch().get().getGroup(Constants.SecondGroupName).captures.length; i++) { + Capture secondCapture = match.getMatch().get().getGroup(Constants.SecondGroupName).captures[i]; + if (secondCapture.index >= time1StartIndex && (secondCapture.index + secondCapture.length) <= time1EndIndex) { + beginSecond = Integer.parseInt(secondCapture.value); + } else if (secondCapture.index >= time2StartIndex && (secondCapture.index + secondCapture.length) <= time2EndIndex) { + endSecond = Integer.parseInt(secondCapture.value); + } + } + + // Desc here means descriptions like "am / pm / o'clock" + // Get leftDesc (if exists) and rightDesc (if exists) + String leftDesc = match.getMatch().get().getGroup("leftDesc").value; + String rightDesc = match.getMatch().get().getGroup("rightDesc").value; + + for (int i = 0; i < match.getMatch().get().getGroup(Constants.DescGroupName).captures.length; i++) { + Capture descCapture = match.getMatch().get().getGroup(Constants.DescGroupName).captures[i]; + if (descCapture.index >= time1StartIndex && (descCapture.index + descCapture.length) <= time1EndIndex && StringUtility.isNullOrEmpty(leftDesc)) { + leftDesc = descCapture.value; + } else if (descCapture.index >= time2StartIndex && (descCapture.index + descCapture.length) <= time2EndIndex && StringUtility.isNullOrEmpty(rightDesc)) { + rightDesc = descCapture.value; + } + } + + LocalDateTime beginDateTime = DateUtil.safeCreateFromMinValue( + year, + month, + day, + beginHour, + beginMinute >= 0 ? beginMinute : 0, + beginSecond >= 0 ? beginSecond : 0); + + LocalDateTime endDateTime = DateUtil.safeCreateFromMinValue( + year, + month, + day, + endHour, + endMinute >= 0 ? endMinute : 0, + endSecond >= 0 ? endSecond : 0); + + boolean hasLeftAm = !StringUtility.isNullOrEmpty(leftDesc) && leftDesc.toLowerCase().startsWith("a"); + boolean hasLeftPm = !StringUtility.isNullOrEmpty(leftDesc) && leftDesc.toLowerCase().startsWith("p"); + boolean hasRightAm = !StringUtility.isNullOrEmpty(rightDesc) && rightDesc.toLowerCase().startsWith("a"); + boolean hasRightPm = !StringUtility.isNullOrEmpty(rightDesc) && rightDesc.toLowerCase().startsWith("p"); + boolean hasLeft = hasLeftAm || hasLeftPm; + boolean hasRight = hasRightAm || hasRightPm; + + // Both timepoint has description like 'am' or 'pm' + if (hasLeft && hasRight) { + if (hasLeftAm) { + if (beginHour >= Constants.HalfDayHourCount) { + beginDateTime = beginDateTime.minusHours(Constants.HalfDayHourCount); + } + } else if (hasLeftPm) { + if (beginHour < Constants.HalfDayHourCount) { + beginDateTime = beginDateTime.plusHours(Constants.HalfDayHourCount); + } + } + + if (hasRightAm) { + if (endHour > Constants.HalfDayHourCount) { + endDateTime = endDateTime.minusHours(Constants.HalfDayHourCount); + } + } else if (hasRightPm) { + if (endHour < Constants.HalfDayHourCount) { + endDateTime = endDateTime.plusHours(Constants.HalfDayHourCount); + } + } + } else if (hasLeft || hasRight) { // one of the timepoint has description like 'am' or 'pm' + if (hasLeftAm) { + if (beginHour >= Constants.HalfDayHourCount) { + beginDateTime = beginDateTime.minusHours(Constants.HalfDayHourCount); + } + + if (endHour < Constants.HalfDayHourCount) { + if (endDateTime.isBefore(beginDateTime)) { + endDateTime = endDateTime.plusHours(Constants.HalfDayHourCount); + } + } + } else if (hasLeftPm) { + if (beginHour < Constants.HalfDayHourCount) { + beginDateTime = beginDateTime.plusHours(Constants.HalfDayHourCount); + } + + if (endHour < Constants.HalfDayHourCount) { + if (endDateTime.isBefore(beginDateTime)) { + Duration span = Duration.between(endDateTime, beginDateTime).abs(); + if (span.toHours() >= Constants.HalfDayHourCount) { + endDateTime = endDateTime.plusHours(24); + } else { + endDateTime = endDateTime.plusHours(Constants.HalfDayHourCount); + } + } + } + } + + if (hasRightAm) { + if (endHour >= Constants.HalfDayHourCount) { + endDateTime = endDateTime.minusHours(Constants.HalfDayHourCount); + } + + if (beginHour < Constants.HalfDayHourCount) { + if (endDateTime.isBefore(beginDateTime)) { + beginDateTime = beginDateTime.minusHours(Constants.HalfDayHourCount); + } + } + } else if (hasRightPm) { + if (endHour < Constants.HalfDayHourCount) { + endDateTime = endDateTime.plusHours(Constants.HalfDayHourCount); + } + + if (beginHour < Constants.HalfDayHourCount) { + if (endDateTime.isBefore(beginDateTime)) { + beginDateTime = beginDateTime.minusHours(Constants.HalfDayHourCount); + } else { + Duration span = Duration.between(beginDateTime, endDateTime); + if (span.toHours() > Constants.HalfDayHourCount) { + beginDateTime = beginDateTime.plusHours(Constants.HalfDayHourCount); + } + } + } + } + } else if (beginHour <= Constants.HalfDayHourCount && endHour <= Constants.HalfDayHourCount) { + // No 'am' or 'pm' indicator + if (beginHour > endHour) { + if (beginHour == Constants.HalfDayHourCount) { + beginDateTime = beginDateTime.minusHours(Constants.HalfDayHourCount); + } else { + endDateTime = endDateTime.plusHours(Constants.HalfDayHourCount); + } + } + ret.setComment(Constants.Comment_AmPm); + } + + if (endDateTime.isBefore(beginDateTime)) { + endDateTime = endDateTime.plusHours(24); + } + + String beginStr = DateTimeFormatUtil.shortTime(beginDateTime.getHour(), beginMinute, beginSecond); + String endStr = DateTimeFormatUtil.shortTime(endDateTime.getHour(), endMinute, endSecond); + + ret.setSuccess(true); + + ret.setTimex(String.format("(%s,%s,%s)", beginStr, endStr, DateTimeFormatUtil.luisTimeSpan(Duration.between(beginDateTime, endDateTime)))); + + ret.setFutureValue(new Pair(beginDateTime, endDateTime)); + ret.setPastValue(new Pair(beginDateTime, endDateTime)); + + List subDateTimeEntities = new ArrayList<>(); + + // In SplitDateAndTime mode, time points will be get from these SubDateTimeEntities + // Cases like "from 4 to 5pm", "4" should not be treated as SubDateTimeEntity + if (hasLeft || beginMinute != Constants.InvalidMinute || beginSecond != Constants.InvalidSecond) { + ExtractResult er = new ExtractResult( + time1StartIndex, + time1EndIndex - time1StartIndex, + text.substring(time1StartIndex, time1EndIndex), + Constants.SYS_DATETIME_TIME); + + DateTimeParseResult pr = this.config.getTimeParser().parse(er, referenceTime); + subDateTimeEntities.add(pr); + } + + // Cases like "from 4am to 5", "5" should not be treated as SubDateTimeEntity + if (hasRight || endMinute != Constants.InvalidMinute || endSecond != Constants.InvalidSecond) { + ExtractResult er = new ExtractResult( + + time2StartIndex, + time2EndIndex - time2StartIndex, + text.substring(time2StartIndex, time2EndIndex), + Constants.SYS_DATETIME_TIME + ); + + DateTimeParseResult pr = this.config.getTimeParser().parse(er, referenceTime); + subDateTimeEntities.add(pr); + } + ret.setSubDateTimeEntities(subDateTimeEntities); + ret.setSuccess(true); + } + + return ret; + } + + private DateTimeResolutionResult mergeTwoTimePoints(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + DateTimeParseResult pr1 = null; + DateTimeParseResult pr2 = null; + boolean validTimeNumber = false; + + List ers = this.config.getTimeExtractor().extract(text, referenceTime); + if (ers.size() != 2) { + if (ers.size() == 1) { + List numErs = this.config.getIntegerExtractor().extract(text); + int erStart = ers.get(0).getStart() != null ? ers.get(0).getStart() : 0; + int erLength = ers.get(0).getLength() != null ? ers.get(0).getLength() : 0; + + for (ExtractResult num : numErs) { + int numStart = num.getStart() != null ? num.getStart() : 0; + int numLength = num.getLength() != null ? num.getLength() : 0; + int midStrBegin = 0; + int midStrEnd = 0; + // ending number + if (numStart > erStart + erLength) { + midStrBegin = erStart + erLength; + midStrEnd = numStart - midStrBegin; + } else if (numStart + numLength < erStart) { + midStrBegin = numStart + numLength; + midStrEnd = erStart - midStrBegin; + } + + // check if the middle string between the time point and the valid number is a connect string. + String middleStr = text.substring(midStrBegin, midStrBegin + midStrEnd); + Optional tillMatch = Arrays.stream(RegExpUtility.getMatches(this.config.getTillRegex(), middleStr)).findFirst(); + if (tillMatch.isPresent()) { + num.setData(null); + num.setType(Constants.SYS_DATETIME_TIME); + ers.add(num); + validTimeNumber = true; + break; + } + } + + ers.sort(Comparator.comparingInt(x -> x.getStart())); + } + + if (!validTimeNumber) { + return ret; + } + } + + pr1 = this.config.getTimeParser().parse(ers.get(0), referenceTime); + pr2 = this.config.getTimeParser().parse(ers.get(1), referenceTime); + + if (pr1.getValue() == null || pr2.getValue() == null) { + return ret; + } + + String ampmStr1 = ((DateTimeResolutionResult)pr1.getValue()).getComment(); + String ampmStr2 = ((DateTimeResolutionResult)pr2.getValue()).getComment(); + + LocalDateTime beginTime = (LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getFutureValue(); + LocalDateTime endTime = (LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getFutureValue(); + + if (!StringUtility.isNullOrEmpty(ampmStr2) && ampmStr2.endsWith(Constants.Comment_AmPm) && + (endTime.compareTo(beginTime) < 1) && endTime.plusHours(Constants.HalfDayHourCount).isAfter(beginTime)) { + endTime = endTime.plusHours(Constants.HalfDayHourCount); + ((DateTimeResolutionResult)pr2.getValue()).setFutureValue(endTime); + pr2.setTimexStr(String.format("T%s", endTime.getHour())); + if (endTime.getMinute() > 0) { + pr2.setTimexStr(String.format("%s:%s", pr2.getTimexStr(), endTime.getMinute())); + } + } + + if (!StringUtility.isNullOrEmpty(ampmStr1) && ampmStr1.endsWith(Constants.Comment_AmPm) && endTime.isAfter(beginTime.plusHours(Constants.HalfDayHourCount))) { + beginTime = beginTime.plusHours(Constants.HalfDayHourCount); + ((DateTimeResolutionResult)pr1.getValue()).setFutureValue(beginTime); + pr1.setTimexStr(String.format("T%s", beginTime.getHour())); + if (beginTime.getMinute() > 0) { + pr1.setTimexStr(String.format("%s:%s", pr1.getTimexStr(), beginTime.getMinute())); + } + } + + if (endTime.isBefore(beginTime)) { + endTime = endTime.plusDays(1); + } + + long minutes = (Duration.between(beginTime, endTime).toMinutes() % 60); + long hours = (Duration.between(beginTime, endTime).toHours() % 24); + ret.setTimex(String.format("(%s,%s,PT", pr1.getTimexStr(), pr2.getTimexStr())); + + if (hours > 0) { + ret.setTimex(String.format("%s%sH", ret.getTimex(), hours)); + } + if (minutes > 0) { + ret.setTimex(String.format("%s%sM", ret.getTimex(), minutes)); + } + ret.setTimex(ret.getTimex() + ")"); + + ret.setFutureValue(new Pair(beginTime, endTime)); + ret.setPastValue(new Pair(beginTime, endTime)); + ret.setSuccess(true); + + if (!StringUtility.isNullOrEmpty(ampmStr1) && ampmStr1.endsWith(Constants.Comment_AmPm) && + !StringUtility.isNullOrEmpty(ampmStr2) && ampmStr2.endsWith(Constants.Comment_AmPm)) { + ret.setComment(Constants.Comment_AmPm); + } + + if (((DateTimeResolutionResult)pr1.getValue()).getTimeZoneResolution() != null) { + ret.setTimeZoneResolution(((DateTimeResolutionResult)pr1.getValue()).getTimeZoneResolution()); + } else if (((DateTimeResolutionResult)pr2.getValue()).getTimeZoneResolution() != null) { + ret.setTimeZoneResolution(((DateTimeResolutionResult)pr2.getValue()).getTimeZoneResolution()); + } + + List subDateTimeEntities = new ArrayList<>(); + subDateTimeEntities.add(pr1); + subDateTimeEntities.add(pr2); + ret.setSubDateTimeEntities(subDateTimeEntities); + + return ret; + } + + private DateTimeResolutionResult parseTimeOfDay(String text, LocalDateTime referenceTime) { + int day = referenceTime.getDayOfMonth(); + int month = referenceTime.getMonthValue(); + int year = referenceTime.getYear(); + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + + // extract early/late prefix from text + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config.getTimeOfDayRegex(), text)).findFirst(); + boolean hasEarly = false; + boolean hasLate = false; + if (match.isPresent()) { + if (!StringUtility.isNullOrEmpty(match.get().getGroup("early").value)) { + String early = match.get().getGroup("early").value; + text = text.replace(early, ""); + hasEarly = true; + ret.setComment(Constants.Comment_Early); + ret.setMod(Constants.EARLY_MOD); + } + + if (!hasEarly && !StringUtility.isNullOrEmpty(match.get().getGroup("late").value)) { + String late = match.get().getGroup("late").value; + text = text.replace(late, ""); + hasLate = true; + ret.setComment(Constants.Comment_Late); + ret.setMod(Constants.LATE_MOD); + } + } + MatchedTimeRangeResult timexResult = this.config.getMatchedTimexRange(text, "", 0, 0, 0); + if (!timexResult.getMatched()) { + return new DateTimeResolutionResult(); + } + + // modify time period if "early" or "late" is existed + if (hasEarly) { + timexResult.setEndHour(timexResult.getBeginHour() + 2); + // handling case: night end with 23:59 + if (timexResult.getEndMin() == 59) { + timexResult.setEndMin(0); + } + } else if (hasLate) { + timexResult.setBeginHour(timexResult.getBeginHour() + 2); + } + + ret.setTimex(timexResult.getTimeStr()); + + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromValue(LocalDateTime.MIN, year, month, day, timexResult.getBeginHour(), 0, 0), + DateUtil.safeCreateFromValue(LocalDateTime.MIN, year, month, day, timexResult.getEndHour(), timexResult.getEndMin(), timexResult.getEndMin()))); + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromValue(LocalDateTime.MIN, year, month, day, timexResult.getBeginHour(), 0, 0), + DateUtil.safeCreateFromValue(LocalDateTime.MIN, year, month, day, timexResult.getEndHour(), timexResult.getEndMin(), timexResult.getEndMin()))); + + ret.setSuccess(true); + + return ret; + } + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimeZoneParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimeZoneParser.java new file mode 100644 index 000000000..79bd6a34d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/BaseTimeZoneParser.java @@ -0,0 +1,168 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.ParseResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.resources.EnglishTimeZone; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.TimeZoneResolutionResult; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BaseTimeZoneParser implements IDateTimeParser { + private final Pattern directUtcRegex; + + public BaseTimeZoneParser() { + directUtcRegex = RegExpUtility.getSafeRegExp(EnglishTimeZone.DirectUtcRegex); + } + + @Override + public String getParserName() { + return Constants.SYS_DATETIME_TIME; + } + + @Override + public List filterResults(String query, List candidateResults) { + return candidateResults; + } + + public String normalizeText(String text) { + text = text.replaceAll("\\s+", " "); + text = text.replaceAll("time$|timezone$", ""); + return text.trim(); + } + + @Override + public ParseResult parse(ExtractResult extractResult) { + return this.parse(extractResult, LocalDateTime.now()); + } + + @Override + public DateTimeParseResult parse(ExtractResult er, LocalDateTime reference) { + DateTimeParseResult result; + result = new DateTimeParseResult(er); + + String text = er.getText().toLowerCase(); + String normalizedText = normalizeText(text); + Matcher match = directUtcRegex.matcher(text); + String matched = match.find() ? match.group(2) : ""; + int offsetInMinutes = matched != null ? computeMinutes(matched) : Constants.InvalidOffsetValue; + + if (offsetInMinutes != Constants.InvalidOffsetValue) { + DateTimeResolutionResult value = getDateTimeResolutionResult(offsetInMinutes, text); + String resolutionStr = String.format("%s: %d", Constants.UtcOffsetMinsKey, offsetInMinutes); + + result.setValue(value); + result.setResolutionStr(resolutionStr); + } else if (checkAbbrToMin(normalizedText)) { + int utcMinuteShift = EnglishTimeZone.AbbrToMinMapping.getOrDefault(normalizedText, 0); + + DateTimeResolutionResult value = getDateTimeResolutionResult(utcMinuteShift, text); + String resolutionStr = String.format("%s: %d", Constants.UtcOffsetMinsKey, utcMinuteShift); + + result.setValue(value); + result.setResolutionStr(resolutionStr); + } else if (checkFullToMin(normalizedText)) { + int utcMinuteShift = EnglishTimeZone.FullToMinMapping.getOrDefault(normalizedText, 0); + + DateTimeResolutionResult value = getDateTimeResolutionResult(utcMinuteShift, text); + String resolutionStr = String.format("%s: %d", Constants.UtcOffsetMinsKey, utcMinuteShift); + + result.setValue(value); + result.setResolutionStr(resolutionStr); + } else { + // TODO: Temporary solution for city timezone and ambiguous data + DateTimeResolutionResult value = new DateTimeResolutionResult(); + value.setSuccess(true); + value.setTimeZoneResolution(new TimeZoneResolutionResult("UTC+XX:XX", Constants.InvalidOffsetValue, text)); + String resolutionStr = String.format("%s: %s", Constants.UtcOffsetMinsKey, "XX:XX"); + + result.setValue(value); + result.setResolutionStr(resolutionStr); + } + + return result; + } + + private boolean checkAbbrToMin(String text) { + if (EnglishTimeZone.AbbrToMinMapping.containsKey(text)) { + return EnglishTimeZone.AbbrToMinMapping.get(text) != Constants.InvalidOffsetValue; + } + return false; + } + + private boolean checkFullToMin(String text) { + if (EnglishTimeZone.FullToMinMapping.containsKey(text)) { + return EnglishTimeZone.FullToMinMapping.get(text) != Constants.InvalidOffsetValue; + } + return false; + } + + private DateTimeResolutionResult getDateTimeResolutionResult(int offsetInMinutes, String text) { + DateTimeResolutionResult value = new DateTimeResolutionResult(); + value.setSuccess(true); + value.setTimeZoneResolution(new TimeZoneResolutionResult(convertOffsetInMinsToOffsetString(offsetInMinutes), offsetInMinutes, text)); + return value; + } + + private String convertOffsetInMinsToOffsetString(int offsetInMinutes) { + return String.format("UTC%s%s", offsetInMinutes >= 0 ? "+" : "-", convertMinsToRegularFormat(Math.abs(offsetInMinutes))); + } + + private String convertMinsToRegularFormat(int offsetMins) { + Duration duration = Duration.ofMinutes(offsetMins); + return String.format("%02d:%02d", duration.toHours() % 24, duration.toMinutes() % 60); + } + + // Compute UTC offset in minutes from matched timezone offset in text. e.g. "-4:30" -> -270; "+8"-> 480. + public int computeMinutes(String utcOffset) { + if (utcOffset.length() == 0) { + return Constants.InvalidOffsetValue; + } + + utcOffset = utcOffset.trim(); + int sign = Constants.PositiveSign; // later than utc, default value + if (utcOffset.startsWith("+") || utcOffset.startsWith("-") || utcOffset.startsWith("±")) { + if (utcOffset.startsWith("-")) { + sign = Constants.NegativeSign; // earlier than utc 0 + } + + utcOffset = utcOffset.substring(1).trim(); + } + + int hours = 0; + final int minutes; + if (utcOffset.contains(":")) { + String[] tokens = utcOffset.split(":"); + hours = Integer.parseInt(tokens[0]); + minutes = Integer.parseInt(tokens[1]); + } else { + minutes = 0; + try { + hours = Integer.parseInt(utcOffset); + } catch (Exception e) { + hours = 0; + } + } + + if (hours > Constants.HalfDayHourCount) { + return Constants.InvalidOffsetValue; + } + + if (Arrays.stream(new int[]{0, 15, 30, 45, 60}).anyMatch(x -> x == minutes)) { + return Constants.InvalidOffsetValue; + } + + int offsetInMinutes = hours * 60 + minutes; + offsetInMinutes *= sign; + + return offsetInMinutes; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/DateTimeParseResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/DateTimeParseResult.java new file mode 100644 index 000000000..62b6a9f17 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/DateTimeParseResult.java @@ -0,0 +1,37 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.Metadata; +import com.microsoft.recognizers.text.ParseResult; + +public class DateTimeParseResult extends ParseResult { + //TimexStr is only used in extractors related with date and time + //It will output the TIMEX representation of a time string. + private String timexStr; + + public DateTimeParseResult(Integer start, Integer length, String text, String type, Object data, Object value, String resolutionStr, String timexStr) { + super(start, length, text, type, data, value, resolutionStr); + this.timexStr = timexStr; + } + + public DateTimeParseResult(ExtractResult er) { + this(er.getStart(), er.getLength(), er.getText(), er.getType(), er.getData(), null, null, null); + } + + public DateTimeParseResult(ParseResult pr) { + this(pr.getStart(), pr.getLength(), pr.getText(), pr.getType(), pr.getData(), pr.getValue(), pr.getResolutionStr(), null); + } + + public DateTimeParseResult(Integer start, Integer length, String text, String type, Object data, Object value, String resolutionStr, String timexStr, Metadata metadata) { + super(start, length, text, type, data, value, resolutionStr, metadata); + this.timexStr = timexStr; + } + + public String getTimexStr() { + return timexStr; + } + + public void setTimexStr(String timexStr) { + this.timexStr = timexStr; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/IDateTimeParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/IDateTimeParser.java new file mode 100644 index 000000000..e88ae76da --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/IDateTimeParser.java @@ -0,0 +1,15 @@ +package com.microsoft.recognizers.text.datetime.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.IParser; + +import java.time.LocalDateTime; +import java.util.List; + +public interface IDateTimeParser extends IParser { + String getParserName(); + + DateTimeParseResult parse(ExtractResult er, LocalDateTime reference); + + List filterResults(String query, List candidateResults); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/BaseDateParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/BaseDateParserConfiguration.java new file mode 100644 index 000000000..c72112364 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/BaseDateParserConfiguration.java @@ -0,0 +1,17 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; + +public abstract class BaseDateParserConfiguration extends BaseOptionsConfiguration implements ICommonDateTimeParserConfiguration { + protected BaseDateParserConfiguration(DateTimeOptions options) { + super(options); + } + + @Override + public ImmutableMap getDayOfMonth() { + return BaseDateTime.DayOfMonthDictionary; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ICommonDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ICommonDateTimeParserConfiguration.java new file mode 100644 index 000000000..2696ac12c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ICommonDateTimeParserConfiguration.java @@ -0,0 +1,80 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.regex.Pattern; + +public interface ICommonDateTimeParserConfiguration extends IOptionsConfiguration { + IExtractor getCardinalExtractor(); + + IExtractor getIntegerExtractor(); + + IExtractor getOrdinalExtractor(); + + IParser getNumberParser(); + + IDateExtractor getDateExtractor(); + + IDateTimeExtractor getTimeExtractor(); + + IDateTimeExtractor getDateTimeExtractor(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeExtractor getDatePeriodExtractor(); + + IDateTimeExtractor getTimePeriodExtractor(); + + IDateTimeExtractor getDateTimePeriodExtractor(); + + IDateTimeParser getDateParser(); + + IDateTimeParser getTimeParser(); + + IDateTimeParser getDateTimeParser(); + + IDateTimeParser getDurationParser(); + + IDateTimeParser getDatePeriodParser(); + + IDateTimeParser getTimePeriodParser(); + + IDateTimeParser getDateTimePeriodParser(); + + IDateTimeParser getDateTimeAltParser(); + + IDateTimeParser getTimeZoneParser(); + + ImmutableMap getMonthOfYear(); + + ImmutableMap getNumbers(); + + ImmutableMap getUnitValueMap(); + + ImmutableMap getSeasonMap(); + + ImmutableMap getSpecialYearPrefixesMap(); + + ImmutableMap getUnitMap(); + + ImmutableMap getCardinalMap(); + + ImmutableMap getDayOfMonth(); + + ImmutableMap getDayOfWeek(); + + ImmutableMap getDoubleNumbers(); + + ImmutableMap getWrittenDecades(); + + ImmutableMap getSpecialDecadeCases(); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateParserConfiguration.java new file mode 100644 index 000000000..0f288f7a6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateParserConfiguration.java @@ -0,0 +1,100 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.List; +import java.util.regex.Pattern; + +public interface IDateParserConfiguration extends IOptionsConfiguration { + String getDateTokenPrefix(); + + IExtractor getIntegerExtractor(); + + IExtractor getOrdinalExtractor(); + + IExtractor getCardinalExtractor(); + + IParser getNumberParser(); + + IDateTimeExtractor getDurationExtractor(); + + IDateExtractor getDateExtractor(); + + IDateTimeParser getDurationParser(); + + Iterable getDateRegexes(); + + Pattern getOnRegex(); + + Pattern getSpecialDayRegex(); + + Pattern getSpecialDayWithNumRegex(); + + Pattern getNextRegex(); + + Pattern getThisRegex(); + + Pattern getLastRegex(); + + Pattern getUnitRegex(); + + Pattern getWeekDayRegex(); + + Pattern getMonthRegex(); + + Pattern getWeekDayOfMonthRegex(); + + Pattern getForTheRegex(); + + Pattern getWeekDayAndDayOfMonthRegex(); + + Pattern getRelativeMonthRegex(); + + Pattern getStrictRelativeRegex(); + + Pattern getYearSuffix(); + + Pattern getRelativeWeekDayRegex(); + + Pattern getRelativeDayRegex(); + + Pattern getNextPrefixRegex(); + + Pattern getPastPrefixRegex(); + + ImmutableMap getUnitMap(); + + ImmutableMap getDayOfMonth(); + + ImmutableMap getDayOfWeek(); + + ImmutableMap getMonthOfYear(); + + ImmutableMap getCardinalMap(); + + List getSameDayTerms(); + + List getPlusOneDayTerms(); + + List getMinusOneDayTerms(); + + List getPlusTwoDayTerms(); + + List getMinusTwoDayTerms(); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); + + Integer getSwiftMonthOrYear(String text); + + Boolean isCardinalLast(String text); + + String normalize(String text); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDatePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDatePeriodParserConfiguration.java new file mode 100644 index 000000000..b32018ec7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDatePeriodParserConfiguration.java @@ -0,0 +1,155 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; + +import java.util.regex.Pattern; + +public interface IDatePeriodParserConfiguration extends IOptionsConfiguration { + String getTokenBeforeDate(); + + IDateExtractor getDateExtractor(); + + IExtractor getCardinalExtractor(); + + IExtractor getOrdinalExtractor(); + + IExtractor getIntegerExtractor(); + + IParser getNumberParser(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeParser getDurationParser(); + + IDateTimeParser getDateParser(); + + Pattern getMonthFrontBetweenRegex(); + + Pattern getBetweenRegex(); + + Pattern getMonthFrontSimpleCasesRegex(); + + Pattern getSimpleCasesRegex(); + + Pattern getOneWordPeriodRegex(); + + Pattern getMonthWithYear(); + + Pattern getMonthNumWithYear(); + + Pattern getYearRegex(); + + Pattern getPastRegex(); + + Pattern getFutureRegex(); + + Pattern getFutureSuffixRegex(); + + Pattern getNumberCombinedWithUnit(); + + Pattern getWeekOfMonthRegex(); + + Pattern getWeekOfYearRegex(); + + Pattern getQuarterRegex(); + + Pattern getQuarterRegexYearFront(); + + Pattern getAllHalfYearRegex(); + + Pattern getSeasonRegex(); + + Pattern getWhichWeekRegex(); + + Pattern getWeekOfRegex(); + + Pattern getMonthOfRegex(); + + Pattern getInConnectorRegex(); + + Pattern getWithinNextPrefixRegex(); + + Pattern getNextPrefixRegex(); + + Pattern getPastPrefixRegex(); + + Pattern getThisPrefixRegex(); + + Pattern getRestOfDateRegex(); + + Pattern getLaterEarlyPeriodRegex(); + + Pattern getWeekWithWeekDayRangeRegex(); + + Pattern getYearPlusNumberRegex(); + + Pattern getDecadeWithCenturyRegex(); + + Pattern getYearPeriodRegex(); + + Pattern getComplexDatePeriodRegex(); + + Pattern getRelativeDecadeRegex(); + + Pattern getReferenceDatePeriodRegex(); + + Pattern getAgoRegex(); + + Pattern getLaterRegex(); + + Pattern getLessThanRegex(); + + Pattern getMoreThanRegex(); + + Pattern getCenturySuffixRegex(); + + Pattern getRelativeRegex(); + + Pattern getUnspecificEndOfRangeRegex(); + + Pattern getNowRegex(); + + ImmutableMap getUnitMap(); + + ImmutableMap getCardinalMap(); + + ImmutableMap getDayOfMonth(); + + ImmutableMap getMonthOfYear(); + + ImmutableMap getSeasonMap(); + + ImmutableMap getSpecialYearPrefixesMap(); + + ImmutableMap getWrittenDecades(); + + ImmutableMap getNumbers(); + + ImmutableMap getSpecialDecadeCases(); + + boolean isFuture(String text); + + boolean isYearToDate(String text); + + boolean isMonthToDate(String text); + + boolean isWeekOnly(String text); + + boolean isWeekend(String text); + + boolean isMonthOnly(String text); + + boolean isYearOnly(String text); + + int getSwiftYear(String text); + + int getSwiftDayOrMonth(String text); + + boolean isLastCardinal(String text); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimeAltParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimeAltParserConfiguration.java new file mode 100644 index 000000000..6b1139546 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimeAltParserConfiguration.java @@ -0,0 +1,17 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; + +public interface IDateTimeAltParserConfiguration { + IDateTimeParser getDateTimeParser(); + + IDateTimeParser getDateParser(); + + IDateTimeParser getTimeParser(); + + IDateTimeParser getDateTimePeriodParser(); + + IDateTimeParser getTimePeriodParser(); + + IDateTimeParser getDatePeriodParser(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimeParserConfiguration.java new file mode 100644 index 000000000..30303ee29 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimeParserConfiguration.java @@ -0,0 +1,70 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultTimex; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.regex.Pattern; + +public interface IDateTimeParserConfiguration extends IOptionsConfiguration { + String getTokenBeforeDate(); + + String getTokenBeforeTime(); + + IDateTimeExtractor getDateExtractor(); + + IDateTimeExtractor getTimeExtractor(); + + IDateTimeParser getDateParser(); + + IDateTimeParser getTimeParser(); + + IExtractor getCardinalExtractor(); + + IExtractor getIntegerExtractor(); + + IParser getNumberParser(); + + IDateTimeExtractor getDurationExtractor(); + + IDateTimeParser getDurationParser(); + + Pattern getNowRegex(); + + Pattern getAMTimeRegex(); + + Pattern getPMTimeRegex(); + + Pattern getSimpleTimeOfTodayAfterRegex(); + + Pattern getSimpleTimeOfTodayBeforeRegex(); + + Pattern getSpecificTimeOfDayRegex(); + + Pattern getSpecificEndOfRegex(); + + Pattern getUnspecificEndOfRegex(); + + Pattern getUnitRegex(); + + Pattern getDateNumberConnectorRegex(); + + ImmutableMap getUnitMap(); + + ImmutableMap getNumbers(); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); + + boolean containsAmbiguousToken(String text, String matchedText); + + ResultTimex getMatchedNowTimex(String text); + + int getSwiftDay(String text); + + int getHour(String text, int hour); +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimePeriodParserConfiguration.java new file mode 100644 index 000000000..d74d1da68 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDateTimePeriodParserConfiguration.java @@ -0,0 +1,84 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; + +import java.util.regex.Pattern; + +public interface IDateTimePeriodParserConfiguration extends IOptionsConfiguration { + String getTokenBeforeDate(); + + IDateTimeExtractor getDateExtractor(); + + IDateTimeExtractor getTimeExtractor(); + + IDateTimeExtractor getDateTimeExtractor(); + + IDateTimeExtractor getTimePeriodExtractor(); + + IDateTimeExtractor getDurationExtractor(); + + IExtractor getCardinalExtractor(); + + IParser getNumberParser(); + + IDateTimeParser getDateParser(); + + IDateTimeParser getTimeParser(); + + IDateTimeParser getDateTimeParser(); + + IDateTimeParser getTimePeriodParser(); + + IDateTimeParser getDurationParser(); + + IDateTimeParser getTimeZoneParser(); + + Pattern getPureNumberFromToRegex(); + + Pattern getPureNumberBetweenAndRegex(); + + Pattern getSpecificTimeOfDayRegex(); + + Pattern getTimeOfDayRegex(); + + Pattern getPastRegex(); + + Pattern getFutureRegex(); + + Pattern getFutureSuffixRegex(); + + Pattern getNumberCombinedWithUnitRegex(); + + Pattern getUnitRegex(); + + Pattern getPeriodTimeOfDayWithDateRegex(); + + Pattern getRelativeTimeUnitRegex(); + + Pattern getRestOfDateTimeRegex(); + + Pattern getAmDescRegex(); + + Pattern getPmDescRegex(); + + Pattern getWithinNextPrefixRegex(); + + Pattern getPrefixDayRegex(); + + Pattern getBeforeRegex(); + + Pattern getAfterRegex(); + + ImmutableMap getUnitMap(); + + ImmutableMap getNumbers(); + + MatchedTimeRangeResult getMatchedTimeRange(String text, String timeStr, int beginHour, int endHour, int endMin); + + int getSwiftPrefix(String text); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDurationParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDurationParserConfiguration.java new file mode 100644 index 000000000..686ba08b2 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IDurationParserConfiguration.java @@ -0,0 +1,44 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; + +import java.util.regex.Pattern; + +public interface IDurationParserConfiguration extends IOptionsConfiguration { + IExtractor getCardinalExtractor(); + + IExtractor getDurationExtractor(); + + IParser getNumberParser(); + + Pattern getNumberCombinedWithUnit(); + + Pattern getAnUnitRegex(); + + Pattern getDuringRegex(); + + Pattern getAllDateUnitRegex(); + + Pattern getHalfDateUnitRegex(); + + Pattern getSuffixAndRegex(); + + Pattern getFollowedUnit(); + + Pattern getConjunctionRegex(); + + Pattern getInexactNumberRegex(); + + Pattern getInexactNumberUnitRegex(); + + Pattern getDurationUnitRegex(); + + ImmutableMap getUnitMap(); + + ImmutableMap getUnitValueMap(); + + ImmutableMap getDoubleNumbers(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IHolidayParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IHolidayParserConfiguration.java new file mode 100644 index 000000000..b93317514 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IHolidayParserConfiguration.java @@ -0,0 +1,24 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.regex.Pattern; + +public interface IHolidayParserConfiguration extends IOptionsConfiguration { + ImmutableMap getVariableHolidaysTimexDictionary(); + + ImmutableMap> getHolidayFuncDictionary(); + + ImmutableMap> getHolidayNames(); + + Iterable getHolidayRegexList(); + + int getSwiftYear(String text); + + String sanitizeHolidayToken(String holiday); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IMergedParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IMergedParserConfiguration.java new file mode 100644 index 000000000..8fd654b80 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/IMergedParserConfiguration.java @@ -0,0 +1,29 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.matcher.StringMatcher; + +import java.util.regex.Pattern; + +public interface IMergedParserConfiguration extends ICommonDateTimeParserConfiguration { + Pattern getBeforeRegex(); + + Pattern getAfterRegex(); + + Pattern getSinceRegex(); + + Pattern getAroundRegex(); + + Pattern getSuffixAfterRegex(); + + Pattern getYearRegex(); + + IDateTimeParser getGetParser(); + + IDateTimeParser getHolidayParser(); + + IDateTimeParser getTimeZoneParser(); + + StringMatcher getSuperfluousWordMatcher(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ISetParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ISetParserConfiguration.java new file mode 100644 index 000000000..881b20158 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ISetParserConfiguration.java @@ -0,0 +1,58 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.utilities.MatchedTimexResult; + +import java.util.regex.Pattern; + +public interface ISetParserConfiguration extends IOptionsConfiguration { + IDateTimeExtractor getDurationExtractor(); + + IDateTimeParser getDurationParser(); + + IDateTimeExtractor getTimeExtractor(); + + IDateTimeParser getTimeParser(); + + IDateTimeExtractor getDateExtractor(); + + IDateTimeParser getDateParser(); + + IDateTimeExtractor getDateTimeExtractor(); + + IDateTimeParser getDateTimeParser(); + + IDateTimeExtractor getDatePeriodExtractor(); + + IDateTimeParser getDatePeriodParser(); + + IDateTimeExtractor getTimePeriodExtractor(); + + IDateTimeParser getTimePeriodParser(); + + IDateTimeExtractor getDateTimePeriodExtractor(); + + IDateTimeParser getDateTimePeriodParser(); + + ImmutableMap getUnitMap(); + + Pattern getEachPrefixRegex(); + + Pattern getPeriodicRegex(); + + Pattern getEachUnitRegex(); + + Pattern getEachDayRegex(); + + Pattern getSetWeekDayRegex(); + + Pattern getSetEachRegex(); + + MatchedTimexResult getMatchedDailyTimex(String text); + + MatchedTimexResult getMatchedUnitTimex(String text); +} + diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ITimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ITimeParserConfiguration.java new file mode 100644 index 000000000..1f0854154 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ITimeParserConfiguration.java @@ -0,0 +1,26 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.regex.Pattern; + +public interface ITimeParserConfiguration extends IOptionsConfiguration { + String getTimeTokenPrefix(); + + Pattern getAtRegex(); + + Iterable getTimeRegexes(); + + ImmutableMap getNumbers(); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); + + IDateTimeParser getTimeZoneParser(); + + PrefixAdjustResult adjustByPrefix(String prefix, int hour, int min, boolean hasMin); + + SuffixAdjustResult adjustBySuffix(String suffix, int hour, int min, boolean hasMin, boolean hasAm, boolean hasPm); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ITimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ITimePeriodParserConfiguration.java new file mode 100644 index 000000000..2b8bd57ad --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/ITimePeriodParserConfiguration.java @@ -0,0 +1,40 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; + +import java.util.regex.Pattern; + +public interface ITimePeriodParserConfiguration extends IOptionsConfiguration { + IDateTimeExtractor getTimeExtractor(); + + IDateTimeParser getTimeParser(); + + IExtractor getIntegerExtractor(); + + IDateTimeParser getTimeZoneParser(); + + Pattern getPureNumberFromToRegex(); + + Pattern getPureNumberBetweenAndRegex(); + + Pattern getSpecificTimeFromToRegex(); + + Pattern getSpecificTimeBetweenAndRegex(); + + Pattern getTimeOfDayRegex(); + + Pattern getGeneralEndingRegex(); + + Pattern getTillRegex(); + + ImmutableMap getNumbers(); + + IDateTimeUtilityConfiguration getUtilityConfiguration(); + + MatchedTimeRangeResult getMatchedTimexRange(String text, String timex, int beginHour, int endHour, int endMin); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/MatchedTimeRangeResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/MatchedTimeRangeResult.java new file mode 100644 index 000000000..59f105384 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/MatchedTimeRangeResult.java @@ -0,0 +1,57 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +public class MatchedTimeRangeResult { + private boolean matched; + private String timeStr; + private int beginHour; + private int endHour; + private int endMin; + + public MatchedTimeRangeResult(boolean matched, String timeStr, int beginHour, int endHour, int endMin) { + this.matched = matched; + this.timeStr = timeStr; + this.beginHour = beginHour; + this.endHour = endHour; + this.endMin = endMin; + } + + public boolean getMatched() { + return matched; + } + + public String getTimeStr() { + return timeStr; + } + + public int getBeginHour() { + return beginHour; + } + + public int getEndHour() { + return endHour; + } + + public int getEndMin() { + return endMin; + } + + public void setMatched(boolean matched) { + this.matched = matched; + } + + public void setTimeStr(String timeStr) { + this.timeStr = timeStr; + } + + public void setBeginHour(int beginHour) { + this.beginHour = beginHour; + } + + public void setEndHour(int endHour) { + this.endHour = endHour; + } + + public void setEndMin(int endMin) { + this.endMin = endMin; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/PrefixAdjustResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/PrefixAdjustResult.java new file mode 100644 index 000000000..7887e1c54 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/PrefixAdjustResult.java @@ -0,0 +1,13 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +public class PrefixAdjustResult { + public final int hour; + public final int minute; + public final boolean hasMin; + + public PrefixAdjustResult(int hour, int minute, boolean hasMin) { + this.hour = hour; + this.minute = minute; + this.hasMin = hasMin; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/SuffixAdjustResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/SuffixAdjustResult.java new file mode 100644 index 000000000..86a6a1e34 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/parsers/config/SuffixAdjustResult.java @@ -0,0 +1,17 @@ +package com.microsoft.recognizers.text.datetime.parsers.config; + +public class SuffixAdjustResult { + public final int hour; + public final int minute; + public final boolean hasMin; + public final boolean hasAm; + public final boolean hasPm; + + public SuffixAdjustResult(int hour, int minute, boolean hasMin, boolean hasAm, boolean hasPm) { + this.hour = hour; + this.minute = minute; + this.hasMin = hasMin; + this.hasAm = hasAm; + this.hasPm = hasPm; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/BaseDateTime.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/BaseDateTime.java new file mode 100644 index 000000000..bd4925600 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/BaseDateTime.java @@ -0,0 +1,113 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class BaseDateTime { + + public static final String HourRegex = "(?2[0-4]|[0-1]?\\d)(h)?"; + + public static final String TwoDigitHourRegex = "(?[0-1]\\d|2[0-4])(h)?"; + + public static final String MinuteRegex = "(?[0-5]?\\d)(?!\\d)"; + + public static final String TwoDigitMinuteRegex = "(?[0-5]\\d)(?!\\d)"; + + public static final String DeltaMinuteRegex = "(?[0-5]?\\d)"; + + public static final String SecondRegex = "(?[0-5]?\\d)"; + + public static final String FourDigitYearRegex = "\\b(?((1\\d|20)\\d{2})|2100)(?!\\.0\\b)\\b"; + + public static final String HyphenDateRegex = "((?[0-9]{4})-?(?1[0-2]|0[1-9])-?(?3[01]|0[1-9]|[12][0-9]))|((?1[0-2]|0[1-9])-?(?3[01]|0[1-9]|[12][0-9])-?(?[0-9]{4}))|((?3[01]|0[1-9]|[12][0-9])-?(?1[0-2]|0[1-9])-?(?[0-9]{4}))"; + + public static final String IllegalYearRegex = "([-])({FourDigitYearRegex})([-])" + .replace("{FourDigitYearRegex}", FourDigitYearRegex); + + public static final String CheckDecimalRegex = "(?![,.]\\d)"; + + public static final String RangeConnectorSymbolRegex = "(--|-|—|——|~|–)"; + + public static final String BaseAmDescRegex = "(am\\b|a\\s*\\.\\s*m\\s*\\.|a[\\.]?\\s*m\\b)"; + + public static final String BasePmDescRegex = "(pm\\b|p\\s*\\.\\s*m\\s*\\.|p[\\.]?\\s*m\\b)"; + + public static final String BaseAmPmDescRegex = "(ampm)"; + + public static final String EqualRegex = "(?)="; + + public static final int MinYearNum = 1500; + + public static final int MaxYearNum = 2100; + + public static final int MaxTwoDigitYearFutureNum = 30; + + public static final int MinTwoDigitYearPastNum = 40; + + public static final ImmutableMap DayOfMonthDictionary = ImmutableMap.builder() + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("13", 13) + .put("14", 14) + .put("15", 15) + .put("16", 16) + .put("17", 17) + .put("18", 18) + .put("19", 19) + .put("20", 20) + .put("21", 21) + .put("22", 22) + .put("23", 23) + .put("24", 24) + .put("25", 25) + .put("26", 26) + .put("27", 27) + .put("28", 28) + .put("29", 29) + .put("30", 30) + .put("31", 31) + .build(); + + public static final ImmutableMap VariableHolidaysTimexDictionary = ImmutableMap.builder() + .put("fathers", "-06-WXX-7-3") + .put("mothers", "-05-WXX-7-2") + .put("thanksgiving", "-11-WXX-4-4") + .put("martinlutherking", "-01-WXX-1-3") + .put("washingtonsbirthday", "-02-WXX-1-3") + .put("canberra", "-03-WXX-1-1") + .put("labour", "-09-WXX-1-1") + .put("columbus", "-10-WXX-1-2") + .put("memorial", "-05-WXX-1-4") + .build(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/ChineseDateTime.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/ChineseDateTime.java new file mode 100644 index 000000000..23ebb3069 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/ChineseDateTime.java @@ -0,0 +1,1016 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class ChineseDateTime { + + public static final String LangMarker = "Chi"; + + public static final String MonthRegex = "(?正月|一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月|01月|02月|03月|04月|05月|06月|07月|08月|09月|10月|11月|12月|1月|2月|3月|4月|5月|6月|7月|8月|9月|大年(?!龄|纪|级))"; + + public static final String DayRegex = "(?01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|1|2|3|4|5|6|7|8|9)"; + + public static final String OneToNineIntegerRegex = "[一二三四五六七八九壹贰叁肆伍陆柒捌玖]"; + + public static final String DateDayRegexInChinese = "(?(([12][0-9]|3[01]|[1-9]|[三叁][十拾][一壹]?|[二贰貳]?[十拾]({OneToNineIntegerRegex})?|{OneToNineIntegerRegex})[日号]|初一|三十))" + .replace("{OneToNineIntegerRegex}", OneToNineIntegerRegex); + + public static final String DayRegexNumInChinese = "(?[12][0-9]|3[01]|[1-9]|[三叁][十拾][一壹]?|[二贰貳]?[十拾]({OneToNineIntegerRegex})?|{OneToNineIntegerRegex}|廿|卅)" + .replace("{OneToNineIntegerRegex}", OneToNineIntegerRegex); + + public static final String MonthNumRegex = "(?01|02|03|04|05|06|07|08|09|10|11|12|1|2|3|4|5|6|7|8|9)"; + + public static final String TwoNumYear = "50"; + + public static final String YearNumRegex = "(?((1[5-9]|20)\\d{2})|2100)"; + + public static final String SimpleYearRegex = "(?(\\d{2,4}))"; + + public static final String ZeroToNineIntegerRegexChs = "[一二三四五六七八九零壹贰叁肆伍陆柒捌玖〇两千俩倆仨]"; + + public static final String DynastyStartYear = "元"; + + public static final String RegionTitleRegex = "(贞观|开元|神龙|洪武|建文|永乐|景泰|天顺|成化|嘉靖|万历|崇祯|顺治|康熙|雍正|乾隆|嘉庆|道光|咸丰|同治|光绪|宣统|民国)"; + + public static final String DynastyYearRegex = "(?{RegionTitleRegex})(?({DynastyStartYear}|\\d{1,3}|[十拾]?({ZeroToNineIntegerRegexChs}[十百拾佰]?){0,3}))" + .replace("{RegionTitleRegex}", RegionTitleRegex) + .replace("{DynastyStartYear}", DynastyStartYear) + .replace("{ZeroToNineIntegerRegexChs}", ZeroToNineIntegerRegexChs); + + public static final String DateYearInChineseRegex = "(?({ZeroToNineIntegerRegexChs}{ZeroToNineIntegerRegexChs}{ZeroToNineIntegerRegexChs}{ZeroToNineIntegerRegexChs}|{ZeroToNineIntegerRegexChs}{ZeroToNineIntegerRegexChs}|{ZeroToNineIntegerRegexChs}{ZeroToNineIntegerRegexChs}{ZeroToNineIntegerRegexChs}|{DynastyYearRegex}))" + .replace("{ZeroToNineIntegerRegexChs}", ZeroToNineIntegerRegexChs) + .replace("{DynastyYearRegex}", DynastyYearRegex); + + public static final String WeekDayRegex = "(?周日|周天|周一|周二|周三|周四|周五|周六|星期一|星期二|星期三|星期四|星期五|星期六|星期日|星期天|礼拜一|礼拜二|礼拜三|礼拜四|礼拜五|礼拜六|礼拜日|礼拜天|禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜日|禮拜天|週日|週天|週一|週二|週三|週四|週五|週六)"; + + public static final String LunarRegex = "(农历|初一|正月|大年(?!龄|纪|级))"; + + public static final String DateThisRegex = "(这个|这一个|这|这一|本){WeekDayRegex}" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String DateLastRegex = "(上一个|上个|上一|上|最后一个|最后)(的)?{WeekDayRegex}" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String DateNextRegex = "(下一个|下个|下一|下)(的)?{WeekDayRegex}" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String SpecialDayRegex = "(最近|前天|后天|昨天|明天|今天|今日|明日|昨日|大后天|大前天|後天|大後天)"; + + public static final String SpecialDayWithNumRegex = "^[.]"; + + public static final String WeekDayOfMonthRegex = "((({MonthRegex}|{MonthNumRegex})的\\s*)(?第一个|第二个|第三个|第四个|第五个|最后一个)\\s*{WeekDayRegex})" + .replace("{MonthRegex}", MonthRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String ThisPrefixRegex = "这个|这一个|这|这一|本|今"; + + public static final String LastPrefixRegex = "上个|上一个|上|上一|去"; + + public static final String NextPrefixRegex = "下个|下一个|下|下一|明"; + + public static final String RelativeRegex = "(?({ThisPrefixRegex}|{LastPrefixRegex}|{NextPrefixRegex}))" + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{LastPrefixRegex}", LastPrefixRegex) + .replace("{NextPrefixRegex}", NextPrefixRegex); + + public static final String SpecialDate = "(?({ThisPrefixRegex}|{LastPrefixRegex}|{NextPrefixRegex})年)?(?({ThisPrefixRegex}|{LastPrefixRegex}|{NextPrefixRegex})月)?{DateDayRegexInChinese}" + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{LastPrefixRegex}", LastPrefixRegex) + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{DateDayRegexInChinese}", DateDayRegexInChinese); + + public static final String DateUnitRegex = "(?年|个月|周|日|天)"; + + public static final String BeforeRegex = "以前|之前|前"; + + public static final String AfterRegex = "以后|以後|之后|之後|后|後"; + + public static final String DateRegexList1 = "({LunarRegex}(\\s*))?((({SimpleYearRegex}|{DateYearInChineseRegex})年)(\\s*))?{MonthRegex}(\\s*){DateDayRegexInChinese}((\\s*|,|,){WeekDayRegex})?" + .replace("{LunarRegex}", LunarRegex) + .replace("{SimpleYearRegex}", SimpleYearRegex) + .replace("{DateYearInChineseRegex}", DateYearInChineseRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DateDayRegexInChinese}", DateDayRegexInChinese) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String DateRegexList2 = "((({SimpleYearRegex}|{DateYearInChineseRegex})年)(\\s*))?({LunarRegex}(\\s*))?{MonthRegex}(\\s*){DateDayRegexInChinese}((\\s*|,|,){WeekDayRegex})?" + .replace("{MonthRegex}", MonthRegex) + .replace("{DateDayRegexInChinese}", DateDayRegexInChinese) + .replace("{SimpleYearRegex}", SimpleYearRegex) + .replace("{LunarRegex}", LunarRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DateYearInChineseRegex}", DateYearInChineseRegex); + + public static final String DateRegexList3 = "((({SimpleYearRegex}|{DateYearInChineseRegex})年)(\\s*))?({LunarRegex}(\\s*))?{MonthRegex}(\\s*)({DayRegexNumInChinese}|{DayRegex})((\\s*|,|,){WeekDayRegex})?" + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegexNumInChinese}", DayRegexNumInChinese) + .replace("{SimpleYearRegex}", SimpleYearRegex) + .replace("{LunarRegex}", LunarRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DateYearInChineseRegex}", DateYearInChineseRegex) + .replace("{DayRegex}", DayRegex); + + public static final String DateRegexList4 = "{MonthNumRegex}\\s*/\\s*{DayRegex}" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex); + + public static final String DateRegexList5 = "{DayRegex}\\s*/\\s*{MonthNumRegex}" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex); + + public static final String DateRegexList6 = "{MonthNumRegex}\\s*[/\\\\\\-]\\s*{DayRegex}\\s*[/\\\\\\-]\\s*{SimpleYearRegex}" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{SimpleYearRegex}", SimpleYearRegex); + + public static final String DateRegexList7 = "{DayRegex}\\s*[/\\\\\\-\\.]\\s*{MonthNumRegex}\\s*[/\\\\\\-\\.]\\s*{SimpleYearRegex}" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{SimpleYearRegex}", SimpleYearRegex); + + public static final String DateRegexList8 = "{SimpleYearRegex}\\s*[/\\\\\\-\\. ]\\s*{MonthNumRegex}\\s*[/\\\\\\-\\. ]\\s*{DayRegex}" + .replace("{SimpleYearRegex}", SimpleYearRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex); + + public static final String DatePeriodTillRegex = "(?到|至|--|-|—|——|~|–)"; + + public static final String DatePeriodTillSuffixRequiredRegex = "(?与|和)"; + + public static final String DatePeriodDayRegexInChinese = "(?初一|三十|一日|十一日|二十一日|三十一日|二日|三日|四日|五日|六日|七日|八日|九日|十二日|十三日|十四日|十五日|十六日|十七日|十八日|十九日|二十二日|二十三日|二十四日|二十五日|二十六日|二十七日|二十八日|二十九日|一日|十一日|十日|二十一日|二十日|三十一日|三十日|二日|三日|四日|五日|六日|七日|八日|九日|十二日|十三日|十四日|十五日|十六日|十七日|十八日|十九日|二十二日|二十三日|二十四日|二十五日|二十六日|二十七日|二十八日|二十九日|十日|二十日|三十日|10日|11日|12日|13日|14日|15日|16日|17日|18日|19日|1日|20日|21日|22日|23日|24日|25日|26日|27日|28日|29日|2日|30日|31日|3日|4日|5日|6日|7日|8日|9日|一号|十一号|二十一号|三十一号|二号|三号|四号|五号|六号|七号|八号|九号|十二号|十三号|十四号|十五号|十六号|十七号|十八号|十九号|二十二号|二十三号|二十四号|二十五号|二十六号|二十七号|二十八号|二十九号|一号|十一号|十号|二十一号|二十号|三十一号|三十号|二号|三号|四号|五号|六号|七号|八号|九号|十二号|十三号|十四号|十五号|十六号|十七号|十八号|十九号|二十二号|二十三号|二十四号|二十五号|二十六号|二十七号|二十八号|二十九号|十号|二十号|三十号|10号|11号|12号|13号|14号|15号|16号|17号|18号|19号|1号|20号|21号|22号|23号|24号|25号|26号|27号|28号|29号|2号|30号|31号|3号|4号|5号|6号|7号|8号|9号|一|十一|二十一|三十一|二|三|四|五|六|七|八|九|十二|十三|十四|十五|十六|十七|十八|十九|二十二|二十三|二十四|二十五|二十六|二十七|二十八|二十九|一|十一|十|二十一|二十|三十一|三十|二|三|四|五|六|七|八|九|十二|十三|十四|十五|十六|十七|十八|十九|二十二|二十三|二十四|二十五|二十六|二十七|二十八|二十九|十|二十|三十|廿|卅)"; + + public static final String DatePeriodThisRegex = "这个|这一个|这|这一|本"; + + public static final String DatePeriodLastRegex = "上个|上一个|上|上一"; + + public static final String DatePeriodNextRegex = "下个|下一个|下|下一"; + + public static final String RelativeMonthRegex = "(?({DatePeriodThisRegex}|{DatePeriodLastRegex}|{DatePeriodNextRegex})\\s*月)" + .replace("{DatePeriodThisRegex}", DatePeriodThisRegex) + .replace("{DatePeriodLastRegex}", DatePeriodLastRegex) + .replace("{DatePeriodNextRegex}", DatePeriodNextRegex); + + public static final String HalfYearRegex = "((?(上|前)半年)|(?(下|后)半年))"; + + public static final String YearRegex = "(({YearNumRegex})(\\s*年)?|({SimpleYearRegex})\\s*年){HalfYearRegex}?" + .replace("{YearNumRegex}", YearNumRegex) + .replace("{SimpleYearRegex}", SimpleYearRegex) + .replace("{HalfYearRegex}", HalfYearRegex); + + public static final String StrictYearRegex = "({YearRegex}(?=[\\u4E00-\\u9FFF]|\\s|$|\\W))" + .replace("{YearRegex}", YearRegex); + + public static final String YearRegexInNumber = "(?(\\d{4}))"; + + public static final String DatePeriodYearInChineseRegex = "{DateYearInChineseRegex}年{HalfYearRegex}?" + .replace("{DateYearInChineseRegex}", DateYearInChineseRegex) + .replace("{HalfYearRegex}", HalfYearRegex); + + public static final String MonthSuffixRegex = "(?({RelativeMonthRegex}|{MonthRegex}))" + .replace("{RelativeMonthRegex}", RelativeMonthRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String SimpleCasesRegex = "((从)\\s*)?(({YearRegex}|{DatePeriodYearInChineseRegex})\\s*)?{MonthSuffixRegex}({DatePeriodDayRegexInChinese}|{DayRegex})\\s*{DatePeriodTillRegex}\\s*({DatePeriodDayRegexInChinese}|{DayRegex})((\\s+|\\s*,\\s*){YearRegex})?" + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DatePeriodDayRegexInChinese}", DatePeriodDayRegexInChinese) + .replace("{DayRegex}", DayRegex) + .replace("{DatePeriodTillRegex}", DatePeriodTillRegex); + + public static final String YearAndMonth = "({DatePeriodYearInChineseRegex}|{YearRegex})\\s*{MonthRegex}" + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex) + .replace("{YearRegex}", YearRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String PureNumYearAndMonth = "({YearRegexInNumber}\\s*[-\\.\\/]\\s*{MonthNumRegex})|({MonthNumRegex}\\s*\\/\\s*{YearRegexInNumber})" + .replace("{YearRegexInNumber}", YearRegexInNumber) + .replace("{MonthNumRegex}", MonthNumRegex); + + public static final String OneWordPeriodRegex = "(((?(明|今|去)年)\\s*)?{MonthRegex}|({DatePeriodThisRegex}|{DatePeriodLastRegex}|{DatePeriodNextRegex})(?半)?\\s*(周末|周|月|年)|周末|(今|明|去|前|后)年(\\s*{HalfYearRegex})?)" + .replace("{MonthRegex}", MonthRegex) + .replace("{DatePeriodThisRegex}", DatePeriodThisRegex) + .replace("{DatePeriodLastRegex}", DatePeriodLastRegex) + .replace("{DatePeriodNextRegex}", DatePeriodNextRegex) + .replace("{HalfYearRegex}", HalfYearRegex); + + public static final String WeekOfMonthRegex = "(?{MonthSuffixRegex}的(?第一|第二|第三|第四|第五|最后一)\\s*周\\s*)" + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String UnitRegex = "(?年|(个)?月|周|日|天)"; + + public static final String FollowedUnit = "^\\s*{UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String NumberCombinedWithUnit = "(?\\d+(\\.\\d*)?){UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String DateRangePrepositions = "((从|在|自)\\s*)?"; + + public static final String YearToYear = "({DateRangePrepositions})({DatePeriodYearInChineseRegex}|{YearRegex})\\s*({DatePeriodTillRegex}|后|後|之后|之後)\\s*({DatePeriodYearInChineseRegex}|{YearRegex})(\\s*((之间|之内|期间|中间|间)|前|之前))?" + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex) + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodTillRegex}", DatePeriodTillRegex) + .replace("{DateRangePrepositions}", DateRangePrepositions); + + public static final String YearToYearSuffixRequired = "({DateRangePrepositions})({DatePeriodYearInChineseRegex}|{YearRegex})\\s*({DatePeriodTillSuffixRequiredRegex})\\s*({DatePeriodYearInChineseRegex}|{YearRegex})\\s*(之间|之内|期间|中间|间)" + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex) + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodTillSuffixRequiredRegex}", DatePeriodTillSuffixRequiredRegex) + .replace("{DateRangePrepositions}", DateRangePrepositions); + + public static final String MonthToMonth = "({DateRangePrepositions})({MonthRegex}){DatePeriodTillRegex}({MonthRegex})" + .replace("{MonthRegex}", MonthRegex) + .replace("{DatePeriodTillRegex}", DatePeriodTillRegex) + .replace("{DateRangePrepositions}", DateRangePrepositions); + + public static final String MonthToMonthSuffixRequired = "({DateRangePrepositions})({MonthRegex}){DatePeriodTillSuffixRequiredRegex}({MonthRegex})\\s*(之间|之内|期间|中间|间)" + .replace("{MonthRegex}", MonthRegex) + .replace("{DatePeriodTillSuffixRequiredRegex}", DatePeriodTillSuffixRequiredRegex) + .replace("{DateRangePrepositions}", DateRangePrepositions); + + public static final String PastRegex = "(?(之前|前|上|近|过去))"; + + public static final String FutureRegex = "(?(之后|之後|后|後|(?春|夏|秋|冬)(天|季)?"; + + public static final String SeasonWithYear = "(({YearRegex}|{DatePeriodYearInChineseRegex}|(?明年|今年|去年))(的)?)?{SeasonRegex}" + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex) + .replace("{SeasonRegex}", SeasonRegex); + + public static final String QuarterRegex = "(({YearRegex}|{DatePeriodYearInChineseRegex}|(?明年|今年|去年))(的)?)?(第(?1|2|3|4|一|二|三|四)季度)" + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex); + + public static final String CenturyRegex = "(?\\d|1\\d|2\\d)世纪"; + + public static final String CenturyRegexInChinese = "(?一|二|三|四|五|六|七|八|九|十|十一|十二|十三|十四|十五|十六|十七|十八|十九|二十|二十一|二十二)世纪"; + + public static final String RelativeCenturyRegex = "(?({DatePeriodLastRegex}|{DatePeriodThisRegex}|{DatePeriodNextRegex}))世纪" + .replace("{DatePeriodLastRegex}", DatePeriodLastRegex) + .replace("{DatePeriodThisRegex}", DatePeriodThisRegex) + .replace("{DatePeriodNextRegex}", DatePeriodNextRegex); + + public static final String DecadeRegexInChinese = "(?十|一十|二十|三十|四十|五十|六十|七十|八十|九十)"; + + public static final String DecadeRegex = "(?({CenturyRegex}|{CenturyRegexInChinese}|{RelativeCenturyRegex}))?(?(\\d0|{DecadeRegexInChinese}))年代" + .replace("{CenturyRegex}", CenturyRegex) + .replace("{CenturyRegexInChinese}", CenturyRegexInChinese) + .replace("{RelativeCenturyRegex}", RelativeCenturyRegex) + .replace("{DecadeRegexInChinese}", DecadeRegexInChinese); + + public static final String PrepositionRegex = "(?^的|在$)"; + + public static final String NowRegex = "(?现在|马上|立刻|刚刚才|刚刚|刚才|这会儿|当下|此刻)"; + + public static final String NightRegex = "(?早|晚)"; + + public static final String TimeOfTodayRegex = "(今晚|今早|今晨|明晚|明早|明晨|昨晚)(的|在)?"; + + public static final String DateTimePeriodTillRegex = "(?到|直到|--|-|—|——)"; + + public static final String DateTimePeriodPrepositionRegex = "(?^\\s*的|在\\s*$)"; + + public static final String HourRegex = "\\b{BaseDateTime.HourRegex}" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String HourNumRegex = "(?[零〇一二两三四五六七八九]|二十[一二三四]?|十[一二三四五六七八九]?)"; + + public static final String ZhijianRegex = "^\\s*(之间|之内|期间|中间|间)"; + + public static final String DateTimePeriodThisRegex = "这个|这一个|这|这一"; + + public static final String DateTimePeriodLastRegex = "上个|上一个|上|上一"; + + public static final String DateTimePeriodNextRegex = "下个|下一个|下|下一"; + + public static final String AmPmDescRegex = "(?(am|a\\.m\\.|a m|a\\. m\\.|a\\.m|a\\. m|a m|pm|p\\.m\\.|p m|p\\. m\\.|p\\.m|p\\. m|p m))"; + + public static final String TimeOfDayRegex = "(?凌晨|清晨|早上|早间|早|上午|中午|下午|午后|晚上|夜里|夜晚|半夜|夜间|深夜|傍晚|晚)"; + + public static final String SpecificTimeOfDayRegex = "((({DateTimePeriodThisRegex}|{DateTimePeriodNextRegex}|{DateTimePeriodLastRegex})\\s+{TimeOfDayRegex})|(今晚|今早|今晨|明晚|明早|明晨|昨晚))" + .replace("{DateTimePeriodThisRegex}", DateTimePeriodThisRegex) + .replace("{DateTimePeriodNextRegex}", DateTimePeriodNextRegex) + .replace("{DateTimePeriodLastRegex}", DateTimePeriodLastRegex) + .replace("{TimeOfDayRegex}", TimeOfDayRegex); + + public static final String DateTimePeriodUnitRegex = "(个)?(?(小时|钟头|分钟|秒钟|时|分|秒))"; + + public static final String DateTimePeriodFollowedUnit = "^\\s*{DateTimePeriodUnitRegex}" + .replace("{DateTimePeriodUnitRegex}", DateTimePeriodUnitRegex); + + public static final String DateTimePeriodNumberCombinedWithUnit = "\\b(?\\d+(\\.\\d*)?){DateTimePeriodUnitRegex}" + .replace("{DateTimePeriodUnitRegex}", DateTimePeriodUnitRegex); + + public static final String DurationYearRegex = "((\\d{3,4})|0\\d|两千)\\s*年"; + + public static final String DurationHalfSuffixRegex = "半"; + + public static final ImmutableMap DurationSuffixList = ImmutableMap.builder() + .put("M", "分钟") + .put("S", "秒钟|秒") + .put("H", "个小时|小时|个钟头|钟头|时") + .put("D", "天") + .put("W", "星期|个星期|周") + .put("Mon", "个月") + .put("Y", "年") + .build(); + + public static final List DurationAmbiguousUnits = Arrays.asList("分钟", "秒钟", "秒", "个小时", "小时", "天", "星期", "个星期", "周", "个月", "年", "时"); + + public static final String DurationUnitRegex = "(?{DateUnitRegex}|分钟?|秒钟?|个?小时|时|个?钟头|天|个?星期|周|个?月|年)" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String DurationConnectorRegex = "^\\s*(?[多又余零]?)\\s*$"; + + public static final String LunarHolidayRegex = "(({YearRegex}|{DatePeriodYearInChineseRegex}|(?明年|今年|去年))(的)?)?(?除夕|春节|中秋节|中秋|元宵节|端午节|端午|重阳节)" + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex); + + public static final String HolidayRegexList1 = "(({YearRegex}|{DatePeriodYearInChineseRegex}|(?明年|今年|去年))(的)?)?(?新年|五一|劳动节|元旦节|元旦|愚人节|平安夜|圣诞节|植树节|国庆节|情人节|教师节|儿童节|妇女节|青年节|建军节|女生节|光棍节|双十一|清明节|清明)" + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex); + + public static final String HolidayRegexList2 = "(({YearRegex}|{DatePeriodYearInChineseRegex}|(?明年|今年|去年))(的)?)?(?母亲节|父亲节|感恩节|万圣节)" + .replace("{YearRegex}", YearRegex) + .replace("{DatePeriodYearInChineseRegex}", DatePeriodYearInChineseRegex); + + public static final String SetUnitRegex = "(?年|月|周|星期|日|天|小时|时|分钟|分|秒钟|秒)"; + + public static final String SetEachUnitRegex = "(?(每个|每一|每)\\s*{SetUnitRegex})" + .replace("{SetUnitRegex}", SetUnitRegex); + + public static final String SetEachPrefixRegex = "(?(每)\\s*$)"; + + public static final String SetLastRegex = "(?last|this|next)"; + + public static final String SetEachDayRegex = "(每|每一)(天|日)\\s*$"; + + public static final String TimeHourNumRegex = "(00|01|02|03|04|05|06|07|08|09|0|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|1|2|3|4|5|6|7|8|9)"; + + public static final String TimeMinuteNumRegex = "(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|0|1|2|3|4|5|6|7|8|9)"; + + public static final String TimeSecondNumRegex = "(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|0|1|2|3|4|5|6|7|8|9)"; + + public static final String TimeHourChsRegex = "([零〇一二两三四五六七八九]|二十[一二三四]?|十[一二三四五六七八九]?)"; + + public static final String TimeMinuteChsRegex = "([二三四五]?十[一二三四五六七八九]?|六十|[零〇一二三四五六七八九])"; + + public static final String TimeSecondChsRegex = "{TimeMinuteChsRegex}" + .replace("{TimeMinuteChsRegex}", TimeMinuteChsRegex); + + public static final String TimeClockDescRegex = "(点\\s*整|点\\s*钟|点|时)"; + + public static final String TimeMinuteDescRegex = "(分钟|分|)"; + + public static final String TimeSecondDescRegex = "(秒钟|秒)"; + + public static final String TimeBanHourPrefixRegex = "(第)"; + + public static final String TimeHourRegex = "(?{TimeHourChsRegex}|{TimeHourNumRegex}){TimeClockDescRegex}" + .replace("{TimeBanHourPrefixRegex}", TimeBanHourPrefixRegex) + .replace("{TimeHourChsRegex}", TimeHourChsRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{TimeClockDescRegex}", TimeClockDescRegex); + + public static final String TimeMinuteRegex = "(?{TimeMinuteChsRegex}|{TimeMinuteNumRegex}){TimeMinuteDescRegex}" + .replace("{TimeMinuteChsRegex}", TimeMinuteChsRegex) + .replace("{TimeMinuteNumRegex}", TimeMinuteNumRegex) + .replace("{TimeMinuteDescRegex}", TimeMinuteDescRegex); + + public static final String TimeSecondRegex = "(?{TimeSecondChsRegex}|{TimeSecondNumRegex}){TimeSecondDescRegex}" + .replace("{TimeSecondChsRegex}", TimeSecondChsRegex) + .replace("{TimeSecondNumRegex}", TimeSecondNumRegex) + .replace("{TimeSecondDescRegex}", TimeSecondDescRegex); + + public static final String TimeHalfRegex = "(?过半|半)"; + + public static final String TimeQuarterRegex = "(?[一两二三四1-4])\\s*(刻钟|刻)"; + + public static final String TimeChineseTimeRegex = "{TimeHourRegex}({TimeQuarterRegex}|{TimeHalfRegex}|((过|又)?{TimeMinuteRegex})({TimeSecondRegex})?)?" + .replace("{TimeHourRegex}", TimeHourRegex) + .replace("{TimeQuarterRegex}", TimeQuarterRegex) + .replace("{TimeHalfRegex}", TimeHalfRegex) + .replace("{TimeMinuteRegex}", TimeMinuteRegex) + .replace("{TimeSecondRegex}", TimeSecondRegex); + + public static final String TimeDigitTimeRegex = "(?{TimeHourNumRegex}):(?{TimeMinuteNumRegex})(:(?{TimeSecondNumRegex}))?" + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{TimeMinuteNumRegex}", TimeMinuteNumRegex) + .replace("{TimeSecondNumRegex}", TimeSecondNumRegex); + + public static final String TimeDayDescRegex = "(?凌晨|清晨|早上|早间|早|上午|中午|下午|午后|晚上|夜里|夜晚|半夜|午夜|夜间|深夜|傍晚|晚)"; + + public static final String TimeApproximateDescPreffixRegex = "(大[约概]|差不多|可能|也许|约|不超过|不多[于过]|最[多长少]|少于|[超短长多]过|几乎要|将近|差点|快要|接近|至少|起码|超出|不到)"; + + public static final String TimeApproximateDescSuffixRegex = "(左右)"; + + public static final String TimeRegexes1 = "{TimeApproximateDescPreffixRegex}?{TimeDayDescRegex}?{TimeChineseTimeRegex}{TimeApproximateDescSuffixRegex}?" + .replace("{TimeApproximateDescPreffixRegex}", TimeApproximateDescPreffixRegex) + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeChineseTimeRegex}", TimeChineseTimeRegex) + .replace("{TimeApproximateDescSuffixRegex}", TimeApproximateDescSuffixRegex); + + public static final String TimeRegexes2 = "{TimeApproximateDescPreffixRegex}?{TimeDayDescRegex}?{TimeDigitTimeRegex}{TimeApproximateDescSuffixRegex}?(\\s*{AmPmDescRegex}?)" + .replace("{TimeApproximateDescPreffixRegex}", TimeApproximateDescPreffixRegex) + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeDigitTimeRegex}", TimeDigitTimeRegex) + .replace("{TimeApproximateDescSuffixRegex}", TimeApproximateDescSuffixRegex) + .replace("{AmPmDescRegex}", AmPmDescRegex); + + public static final String TimeRegexes3 = "差{TimeMinuteRegex}{TimeChineseTimeRegex}" + .replace("{TimeMinuteRegex}", TimeMinuteRegex) + .replace("{TimeChineseTimeRegex}", TimeChineseTimeRegex); + + public static final String TimePeriodTimePeriodConnectWords = "(起|至|到|–|-|—|~|~)"; + + public static final String TimePeriodLeftChsTimeRegex = "(从)?(?{TimeDayDescRegex}?({TimeChineseTimeRegex}))" + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeChineseTimeRegex}", TimeChineseTimeRegex); + + public static final String TimePeriodRightChsTimeRegex = "{TimePeriodTimePeriodConnectWords}(?{TimeDayDescRegex}?{TimeChineseTimeRegex})(之间)?" + .replace("{TimePeriodTimePeriodConnectWords}", TimePeriodTimePeriodConnectWords) + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeChineseTimeRegex}", TimeChineseTimeRegex); + + public static final String TimePeriodLeftDigitTimeRegex = "(从)?(?{TimeDayDescRegex}?({TimeDigitTimeRegex}))" + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeDigitTimeRegex}", TimeDigitTimeRegex); + + public static final String TimePeriodRightDigitTimeRegex = "{TimePeriodTimePeriodConnectWords}(?{TimeDayDescRegex}?{TimeDigitTimeRegex})(之间)?" + .replace("{TimePeriodTimePeriodConnectWords}", TimePeriodTimePeriodConnectWords) + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeDigitTimeRegex}", TimeDigitTimeRegex); + + public static final String TimePeriodShortLeftChsTimeRegex = "(从)?(?{TimeDayDescRegex}?({TimeHourChsRegex}))" + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeHourChsRegex}", TimeHourChsRegex); + + public static final String TimePeriodShortLeftDigitTimeRegex = "(从)?(?{TimeDayDescRegex}?({TimeHourNumRegex}))" + .replace("{TimeDayDescRegex}", TimeDayDescRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex); + + public static final String TimePeriodRegexes1 = "({TimePeriodLeftDigitTimeRegex}{TimePeriodRightDigitTimeRegex}|{TimePeriodLeftChsTimeRegex}{TimePeriodRightChsTimeRegex})" + .replace("{TimePeriodLeftDigitTimeRegex}", TimePeriodLeftDigitTimeRegex) + .replace("{TimePeriodRightDigitTimeRegex}", TimePeriodRightDigitTimeRegex) + .replace("{TimePeriodLeftChsTimeRegex}", TimePeriodLeftChsTimeRegex) + .replace("{TimePeriodRightChsTimeRegex}", TimePeriodRightChsTimeRegex); + + public static final String TimePeriodRegexes2 = "({TimePeriodShortLeftDigitTimeRegex}{TimePeriodRightDigitTimeRegex}|{TimePeriodShortLeftChsTimeRegex}{TimePeriodRightChsTimeRegex})" + .replace("{TimePeriodShortLeftDigitTimeRegex}", TimePeriodShortLeftDigitTimeRegex) + .replace("{TimePeriodRightDigitTimeRegex}", TimePeriodRightDigitTimeRegex) + .replace("{TimePeriodShortLeftChsTimeRegex}", TimePeriodShortLeftChsTimeRegex) + .replace("{TimePeriodRightChsTimeRegex}", TimePeriodRightChsTimeRegex); + + public static final String FromToRegex = "(从|自).+([至到]).+"; + + public static final String AmbiguousRangeModifierPrefix = "(从|自)"; + + public static final String ParserConfigurationBefore = "((?和|或|及)?(之前|以前)|前)"; + + public static final String ParserConfigurationAfter = "((?和|或|及)?(之后|之後|以后|以後)|后|後)"; + + public static final String ParserConfigurationUntil = "(直到|直至|截至|截止(到)?)"; + + public static final String ParserConfigurationSincePrefix = "(自从|自|自打|打|从)"; + + public static final String ParserConfigurationSinceSuffix = "(以来|开始|起)"; + + public static final String ParserConfigurationLastWeekDayToken = "最后一个"; + + public static final String ParserConfigurationNextMonthToken = "下一个"; + + public static final String ParserConfigurationLastMonthToken = "上一个"; + + public static final String ParserConfigurationDatePrefix = " "; + + public static final ImmutableMap ParserConfigurationUnitMap = ImmutableMap.builder() + .put("年", "Y") + .put("月", "MON") + .put("个月", "MON") + .put("日", "D") + .put("周", "W") + .put("天", "D") + .put("小时", "H") + .put("个小时", "H") + .put("时", "H") + .put("分钟", "M") + .put("分", "M") + .put("秒钟", "S") + .put("秒", "S") + .put("星期", "W") + .put("个星期", "W") + .build(); + + public static final ImmutableMap ParserConfigurationUnitValueMap = ImmutableMap.builder() + .put("years", 31536000L) + .put("year", 31536000L) + .put("months", 2592000L) + .put("month", 2592000L) + .put("weeks", 604800L) + .put("week", 604800L) + .put("days", 86400L) + .put("day", 86400L) + .put("hours", 3600L) + .put("hour", 3600L) + .put("hrs", 3600L) + .put("hr", 3600L) + .put("h", 3600L) + .put("minutes", 60L) + .put("minute", 60L) + .put("mins", 60L) + .put("min", 60L) + .put("seconds", 1L) + .put("second", 1L) + .put("secs", 1L) + .put("sec", 1L) + .build(); + + public static final List MonthTerms = Arrays.asList("月"); + + public static final List WeekendTerms = Arrays.asList("周末"); + + public static final List WeekTerms = Arrays.asList("周", "星期"); + + public static final List YearTerms = Arrays.asList("年"); + + public static final List ThisYearTerms = Arrays.asList("今年"); + + public static final List LastYearTerms = Arrays.asList("去年"); + + public static final List NextYearTerms = Arrays.asList("明年"); + + public static final List YearAfterNextTerms = Arrays.asList("后年"); + + public static final List YearBeforeLastTerms = Arrays.asList("前年"); + + public static final ImmutableMap ParserConfigurationSeasonMap = ImmutableMap.builder() + .put("春", "SP") + .put("夏", "SU") + .put("秋", "FA") + .put("冬", "WI") + .build(); + + public static final ImmutableMap ParserConfigurationSeasonValueMap = ImmutableMap.builder() + .put("SP", 3) + .put("SU", 6) + .put("FA", 9) + .put("WI", 12) + .build(); + + public static final ImmutableMap ParserConfigurationCardinalMap = ImmutableMap.builder() + .put("一", 1) + .put("二", 2) + .put("三", 3) + .put("四", 4) + .put("五", 5) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("第一个", 1) + .put("第二个", 2) + .put("第三个", 3) + .put("第四个", 4) + .put("第五个", 5) + .put("第一", 1) + .put("第二", 2) + .put("第三", 3) + .put("第四", 4) + .put("第五", 5) + .build(); + + public static final ImmutableMap ParserConfigurationDayOfMonth = ImmutableMap.builder() + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("13", 13) + .put("14", 14) + .put("15", 15) + .put("16", 16) + .put("17", 17) + .put("18", 18) + .put("19", 19) + .put("20", 20) + .put("21", 21) + .put("22", 22) + .put("23", 23) + .put("24", 24) + .put("25", 25) + .put("26", 26) + .put("27", 27) + .put("28", 28) + .put("29", 29) + .put("30", 30) + .put("31", 31) + .put("1日", 1) + .put("2日", 2) + .put("3日", 3) + .put("4日", 4) + .put("5日", 5) + .put("6日", 6) + .put("7日", 7) + .put("8日", 8) + .put("9日", 9) + .put("10日", 10) + .put("11日", 11) + .put("12日", 12) + .put("13日", 13) + .put("14日", 14) + .put("15日", 15) + .put("16日", 16) + .put("17日", 17) + .put("18日", 18) + .put("19日", 19) + .put("20日", 20) + .put("21日", 21) + .put("22日", 22) + .put("23日", 23) + .put("24日", 24) + .put("25日", 25) + .put("26日", 26) + .put("27日", 27) + .put("28日", 28) + .put("29日", 29) + .put("30日", 30) + .put("31日", 31) + .put("一日", 1) + .put("十一日", 11) + .put("二十日", 20) + .put("十日", 10) + .put("二十一日", 21) + .put("三十一日", 31) + .put("二日", 2) + .put("三日", 3) + .put("四日", 4) + .put("五日", 5) + .put("六日", 6) + .put("七日", 7) + .put("八日", 8) + .put("九日", 9) + .put("十二日", 12) + .put("十三日", 13) + .put("十四日", 14) + .put("十五日", 15) + .put("十六日", 16) + .put("十七日", 17) + .put("十八日", 18) + .put("十九日", 19) + .put("二十二日", 22) + .put("二十三日", 23) + .put("二十四日", 24) + .put("二十五日", 25) + .put("二十六日", 26) + .put("二十七日", 27) + .put("二十八日", 28) + .put("二十九日", 29) + .put("三十日", 30) + .put("1号", 1) + .put("2号", 2) + .put("3号", 3) + .put("4号", 4) + .put("5号", 5) + .put("6号", 6) + .put("7号", 7) + .put("8号", 8) + .put("9号", 9) + .put("10号", 10) + .put("11号", 11) + .put("12号", 12) + .put("13号", 13) + .put("14号", 14) + .put("15号", 15) + .put("16号", 16) + .put("17号", 17) + .put("18号", 18) + .put("19号", 19) + .put("20号", 20) + .put("21号", 21) + .put("22号", 22) + .put("23号", 23) + .put("24号", 24) + .put("25号", 25) + .put("26号", 26) + .put("27号", 27) + .put("28号", 28) + .put("29号", 29) + .put("30号", 30) + .put("31号", 31) + .put("一号", 1) + .put("十一号", 11) + .put("二十号", 20) + .put("十号", 10) + .put("二十一号", 21) + .put("三十一号", 31) + .put("二号", 2) + .put("三号", 3) + .put("四号", 4) + .put("五号", 5) + .put("六号", 6) + .put("七号", 7) + .put("八号", 8) + .put("九号", 9) + .put("十二号", 12) + .put("十三号", 13) + .put("十四号", 14) + .put("十五号", 15) + .put("十六号", 16) + .put("十七号", 17) + .put("十八号", 18) + .put("十九号", 19) + .put("二十二号", 22) + .put("二十三号", 23) + .put("二十四号", 24) + .put("二十五号", 25) + .put("二十六号", 26) + .put("二十七号", 27) + .put("二十八号", 28) + .put("二十九号", 29) + .put("三十号", 30) + .put("初一", 32) + .put("三十", 30) + .put("一", 1) + .put("十一", 11) + .put("二十", 20) + .put("十", 10) + .put("二十一", 21) + .put("三十一", 31) + .put("二", 2) + .put("三", 3) + .put("四", 4) + .put("五", 5) + .put("六", 6) + .put("七", 7) + .put("八", 8) + .put("九", 9) + .put("十二", 12) + .put("十三", 13) + .put("十四", 14) + .put("十五", 15) + .put("十六", 16) + .put("十七", 17) + .put("十八", 18) + .put("十九", 19) + .put("二十二", 22) + .put("二十三", 23) + .put("二十四", 24) + .put("二十五", 25) + .put("二十六", 26) + .put("二十七", 27) + .put("二十八", 28) + .put("二十九", 29) + .build(); + + public static final ImmutableMap ParserConfigurationDayOfWeek = ImmutableMap.builder() + .put("星期一", 1) + .put("星期二", 2) + .put("星期三", 3) + .put("星期四", 4) + .put("星期五", 5) + .put("星期六", 6) + .put("星期天", 0) + .put("星期日", 0) + .put("礼拜一", 1) + .put("礼拜二", 2) + .put("礼拜三", 3) + .put("礼拜四", 4) + .put("礼拜五", 5) + .put("礼拜六", 6) + .put("礼拜天", 0) + .put("礼拜日", 0) + .put("周一", 1) + .put("周二", 2) + .put("周三", 3) + .put("周四", 4) + .put("周五", 5) + .put("周六", 6) + .put("周日", 0) + .put("周天", 0) + .put("禮拜一", 1) + .put("禮拜二", 2) + .put("禮拜三", 3) + .put("禮拜四", 4) + .put("禮拜五", 5) + .put("禮拜六", 6) + .put("禮拜天", 0) + .put("禮拜日", 0) + .put("週一", 1) + .put("週二", 2) + .put("週三", 3) + .put("週四", 4) + .put("週五", 5) + .put("週六", 6) + .put("週日", 0) + .put("週天", 0) + .build(); + + public static final ImmutableMap ParserConfigurationMonthOfYear = ImmutableMap.builder() + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .put("一月", 1) + .put("二月", 2) + .put("三月", 3) + .put("四月", 4) + .put("五月", 5) + .put("六月", 6) + .put("七月", 7) + .put("八月", 8) + .put("九月", 9) + .put("十月", 10) + .put("十一月", 11) + .put("十二月", 12) + .put("1月", 1) + .put("2月", 2) + .put("3月", 3) + .put("4月", 4) + .put("5月", 5) + .put("6月", 6) + .put("7月", 7) + .put("8月", 8) + .put("9月", 9) + .put("10月", 10) + .put("11月", 11) + .put("12月", 12) + .put("01月", 1) + .put("02月", 2) + .put("03月", 3) + .put("04月", 4) + .put("05月", 5) + .put("06月", 6) + .put("07月", 7) + .put("08月", 8) + .put("09月", 9) + .put("正月", 13) + .put("大年", 13) + .build(); + + public static final String DateTimeSimpleAmRegex = "(?早|晨)"; + + public static final String DateTimeSimplePmRegex = "(?晚)"; + + public static final String DateTimePeriodMORegex = "(凌晨|清晨|早上|早间|早|上午)"; + + public static final String DateTimePeriodMIRegex = "(中午)"; + + public static final String DateTimePeriodAFRegex = "(下午|午后|傍晚)"; + + public static final String DateTimePeriodEVRegex = "(晚上|夜里|夜晚|晚)"; + + public static final String DateTimePeriodNIRegex = "(半夜|夜间|深夜)"; + + public static final ImmutableMap AmbiguityFiltersDict = ImmutableMap.builder() + .put("早", "(? DurationUnitValueMap = ImmutableMap.builder() + .put("Y", 31536000L) + .put("Mon", 2592000L) + .put("W", 604800L) + .put("D", 86400L) + .put("H", 3600L) + .put("M", 60L) + .put("S", 1L) + .build(); + + public static final ImmutableMap HolidayNoFixedTimex = ImmutableMap.builder() + .put("父亲节", "-06-WXX-6-3") + .put("母亲节", "-05-WXX-7-2") + .put("感恩节", "-11-WXX-4-4") + .build(); + + public static final String MergedBeforeRegex = "(前|之前)$"; + + public static final String MergedAfterRegex = "(后|後|之后|之後)$"; + + public static final ImmutableMap TimeNumberDictionary = ImmutableMap.builder() + .put('零', 0) + .put('一', 1) + .put('二', 2) + .put('三', 3) + .put('四', 4) + .put('五', 5) + .put('六', 6) + .put('七', 7) + .put('八', 8) + .put('九', 9) + .put('〇', 0) + .put('两', 2) + .put('十', 10) + .build(); + + public static final ImmutableMap TimeLowBoundDesc = ImmutableMap.builder() + .put("中午", 11) + .put("下午", 12) + .put("午后", 12) + .put("晚上", 18) + .put("夜里", 18) + .put("夜晚", 18) + .put("夜间", 18) + .put("深夜", 18) + .put("傍晚", 18) + .put("晚", 18) + .put("pm", 12) + .build(); + + public static final String DefaultLanguageFallback = "YMD"; + + public static final List MorningTermList = Arrays.asList("早", "上午", "早间", "早上", "清晨"); + + public static final List MidDayTermList = Arrays.asList("中午", "正午"); + + public static final List AfternoonTermList = Arrays.asList("下午", "午后"); + + public static final List EveningTermList = Arrays.asList("晚", "晚上", "夜里", "傍晚", "夜晚"); + + public static final List DaytimeTermList = Arrays.asList("白天", "日间"); + + public static final List NightTermList = Arrays.asList("深夜"); + + public static final ImmutableMap DynastyYearMap = ImmutableMap.builder() + .put("贞观", 627) + .put("开元", 713) + .put("神龙", 705) + .put("洪武", 1368) + .put("建文", 1399) + .put("永乐", 1403) + .put("景泰", 1450) + .put("天顺", 1457) + .put("成化", 1465) + .put("嘉靖", 1522) + .put("万历", 1573) + .put("崇祯", 1628) + .put("顺治", 1644) + .put("康熙", 1662) + .put("雍正", 1723) + .put("乾隆", 1736) + .put("嘉庆", 1796) + .put("道光", 1821) + .put("咸丰", 1851) + .put("同治", 1862) + .put("光绪", 1875) + .put("宣统", 1909) + .put("民国", 1912) + .build(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/EnglishDateTime.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/EnglishDateTime.java new file mode 100644 index 000000000..c86b81be3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/EnglishDateTime.java @@ -0,0 +1,1465 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class EnglishDateTime { + + public static final String LangMarker = "Eng"; + + public static final Boolean CheckBothBeforeAfter = false; + + public static final String TillRegex = "(?\\b(to|(un)?till?|thru|through)\\b(\\s+the\\b)?|{BaseDateTime.RangeConnectorSymbolRegex})" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String RangeConnectorRegex = "(?\\b(and|through|to)\\b(\\s+the\\b)?|{BaseDateTime.RangeConnectorSymbolRegex})" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String LastNegPrefix = "(?following|next|(up)?coming|this|{LastNegPrefix}last|past|previous|current|the)\\b" + .replace("{LastNegPrefix}", LastNegPrefix); + + public static final String StrictRelativeRegex = "\\b(?following|next|(up)?coming|this|{LastNegPrefix}last|past|previous|current)\\b" + .replace("{LastNegPrefix}", LastNegPrefix); + + public static final String UpcomingPrefixRegex = "((this\\s+)?((up)?coming))"; + + public static final String NextPrefixRegex = "\\b(following|next|{UpcomingPrefixRegex})\\b" + .replace("{UpcomingPrefixRegex}", UpcomingPrefixRegex); + + public static final String AfterNextSuffixRegex = "\\b(after\\s+(the\\s+)?next)\\b"; + + public static final String PastPrefixRegex = "((this\\s+)?past)\\b"; + + public static final String PreviousPrefixRegex = "({LastNegPrefix}last|previous|{PastPrefixRegex})\\b" + .replace("{LastNegPrefix}", LastNegPrefix) + .replace("{PastPrefixRegex}", PastPrefixRegex); + + public static final String ThisPrefixRegex = "(this|current)\\b"; + + public static final String RangePrefixRegex = "(from|between)"; + + public static final String CenturySuffixRegex = "(^century)\\b"; + + public static final String ReferencePrefixRegex = "(that|same)\\b"; + + public static final String FutureSuffixRegex = "\\b(in\\s+the\\s+)?(future|hence)\\b"; + + public static final String DayRegex = "(the\\s*)?(?(?:3[0-1]|[1-2]\\d|0?[1-9])(?:th|nd|rd|st)?)(?=\\b|t)"; + + public static final String ImplicitDayRegex = "(the\\s*)?(?(?:3[0-1]|[0-2]?\\d)(?:th|nd|rd|st))\\b"; + + public static final String MonthNumRegex = "(?1[0-2]|(0)?[1-9])\\b"; + + public static final String WrittenOneToNineRegex = "(?:one|two|three|four|five|six|seven|eight|nine)"; + + public static final String WrittenElevenToNineteenRegex = "(?:eleven|twelve|(?:thir|four|fif|six|seven|eigh|nine)teen)"; + + public static final String WrittenTensRegex = "(?:ten|twenty|thirty|fou?rty|fifty|sixty|seventy|eighty|ninety)"; + + public static final String WrittenNumRegex = "(?:{WrittenOneToNineRegex}|{WrittenElevenToNineteenRegex}|{WrittenTensRegex}(\\s+{WrittenOneToNineRegex})?)" + .replace("{WrittenOneToNineRegex}", WrittenOneToNineRegex) + .replace("{WrittenElevenToNineteenRegex}", WrittenElevenToNineteenRegex) + .replace("{WrittenTensRegex}", WrittenTensRegex); + + public static final String WrittenCenturyFullYearRegex = "(?:(one|two)\\s+thousand(\\s+and)?(\\s+{WrittenOneToNineRegex}\\s+hundred(\\s+and)?)?)" + .replace("{WrittenOneToNineRegex}", WrittenOneToNineRegex); + + public static final String WrittenCenturyOrdinalYearRegex = "(?:twenty(\\s+(one|two))?|ten|eleven|twelve|thirteen|fifteen|eigthteen|(?:four|six|seven|nine)(teen)?|one|two|three|five|eight)"; + + public static final String CenturyRegex = "\\b(?{WrittenCenturyFullYearRegex}|{WrittenCenturyOrdinalYearRegex}(\\s+hundred)?(\\s+and)?)\\b" + .replace("{WrittenCenturyFullYearRegex}", WrittenCenturyFullYearRegex) + .replace("{WrittenCenturyOrdinalYearRegex}", WrittenCenturyOrdinalYearRegex); + + public static final String LastTwoYearNumRegex = "(?:zero\\s+{WrittenOneToNineRegex}|{WrittenElevenToNineteenRegex}|{WrittenTensRegex}(\\s+{WrittenOneToNineRegex})?)" + .replace("{WrittenOneToNineRegex}", WrittenOneToNineRegex) + .replace("{WrittenElevenToNineteenRegex}", WrittenElevenToNineteenRegex) + .replace("{WrittenTensRegex}", WrittenTensRegex); + + public static final String FullTextYearRegex = "\\b((?{CenturyRegex})\\s+(?{LastTwoYearNumRegex})\\b|\\b(?{WrittenCenturyFullYearRegex}|{WrittenCenturyOrdinalYearRegex}\\s+hundred(\\s+and)?))\\b" + .replace("{CenturyRegex}", CenturyRegex) + .replace("{WrittenCenturyFullYearRegex}", WrittenCenturyFullYearRegex) + .replace("{WrittenCenturyOrdinalYearRegex}", WrittenCenturyOrdinalYearRegex) + .replace("{LastTwoYearNumRegex}", LastTwoYearNumRegex); + + public static final String OclockRegex = "(?o\\s*((’|‘|')\\s*)?clock|sharp)"; + + public static final String SpecialDescRegex = "((?)p\\b)"; + + public static final String AmDescRegex = "(?:{BaseDateTime.BaseAmDescRegex})" + .replace("{BaseDateTime.BaseAmDescRegex}", BaseDateTime.BaseAmDescRegex); + + public static final String PmDescRegex = "(:?{BaseDateTime.BasePmDescRegex})" + .replace("{BaseDateTime.BasePmDescRegex}", BaseDateTime.BasePmDescRegex); + + public static final String AmPmDescRegex = "(:?{BaseDateTime.BaseAmPmDescRegex})" + .replace("{BaseDateTime.BaseAmPmDescRegex}", BaseDateTime.BaseAmPmDescRegex); + + public static final String DescRegex = "(:?(:?({OclockRegex}\\s+)?(?({AmPmDescRegex}|{AmDescRegex}|{PmDescRegex}|{SpecialDescRegex})))|{OclockRegex})" + .replace("{OclockRegex}", OclockRegex) + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex) + .replace("{AmPmDescRegex}", AmPmDescRegex) + .replace("{SpecialDescRegex}", SpecialDescRegex); + + public static final String OfPrepositionRegex = "(\\bof\\b)"; + + public static final String TwoDigitYearRegex = "\\b(?([0-9]\\d))(?!(\\s*((\\:\\d)|{AmDescRegex}|{PmDescRegex}|\\.\\d)))\\b" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex); + + public static final String YearRegex = "(?:{BaseDateTime.FourDigitYearRegex}|{FullTextYearRegex})" + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String WeekDayRegex = "\\b(?(?:sun|mon|tues?|thurs?|fri)(day)?|thu|wedn(esday)?|weds?|sat(urday)?)s?\\b"; + + public static final String SingleWeekDayRegex = "\\b(?(?((day\\s+)?of\\s+)?{RelativeRegex}\\s+month)\\b" + .replace("{RelativeRegex}", RelativeRegex); + + public static final String MonthRegex = "\\b(?apr(il)?|aug(ust)?|dec(ember)?|feb(ruary)?|jan(uary)?|july?|june?|mar(ch)?|may|nov(ember)?|oct(ober)?|sept(ember)?|sep)(?!\\p{L})"; + + public static final String WrittenMonthRegex = "(((the\\s+)?month of\\s+)?{MonthRegex})" + .replace("{MonthRegex}", MonthRegex); + + public static final String MonthSuffixRegex = "(?(?:(in|of|on)\\s+)?({RelativeMonthRegex}|{WrittenMonthRegex}))" + .replace("{RelativeMonthRegex}", RelativeMonthRegex) + .replace("{WrittenMonthRegex}", WrittenMonthRegex); + + public static final String DateUnitRegex = "(?decades?|years?|months?|weeks?|(?(business\\s+|week\\s*))?days?|fortnights?|weekends?|(?<=\\s+\\d{1,4})[ymwd])\\b"; + + public static final String DateTokenPrefix = "on "; + + public static final String TimeTokenPrefix = "at "; + + public static final String TokenBeforeDate = "on "; + + public static final String TokenBeforeTime = "at "; + + public static final String FromRegex = "\\b(from(\\s+the)?)$"; + + public static final String BetweenTokenRegex = "\\b(between(\\s+the)?)$"; + + public static final String SimpleCasesRegex = "\\b({RangePrefixRegex}\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex}\\s+{MonthSuffixRegex}|{MonthSuffixRegex}\\s+{DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex) + .replace("{RangePrefixRegex}", RangePrefixRegex); + + public static final String MonthFrontSimpleCasesRegex = "\\b({RangePrefixRegex}\\s+)?{MonthSuffixRegex}\\s+((from)\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{YearRegex}", YearRegex) + .replace("{RangePrefixRegex}", RangePrefixRegex); + + public static final String MonthFrontBetweenRegex = "\\b{MonthSuffixRegex}\\s+(between\\s+)({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{YearRegex}", YearRegex); + + public static final String BetweenRegex = "\\b(between\\s+)({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthWithYear = "\\b(({WrittenMonthRegex}[\\.]?(\\s*)[/\\\\\\-\\.,]?(\\s+(of|in))?(\\s*)({YearRegex}|(?following|next|last|this)\\s+year))|(({YearRegex}|(?following|next|last|this)\\s+year)(\\s*),?(\\s*){WrittenMonthRegex}))\\b" + .replace("{WrittenMonthRegex}", WrittenMonthRegex) + .replace("{YearRegex}", YearRegex); + + public static final String SpecialYearPrefixes = "(calendar|(?fiscal|school))"; + + public static final String OneWordPeriodRegex = "\\b((((the\\s+)?month of\\s+)?({StrictRelativeRegex}\\s+)?{MonthRegex})|(month|year) to date|(?((un)?till?|to)\\s+date)|({RelativeRegex}\\s+)?(my\\s+)?((?working\\s+week|workweek)|week(end)?|month|(({SpecialYearPrefixes}\\s+)?year))(?!((\\s+of)?\\s+\\d+(?!({BaseDateTime.BaseAmDescRegex}|{BaseDateTime.BasePmDescRegex}))|\\s+to\\s+date))(\\s+{AfterNextSuffixRegex})?)\\b" + .replace("{StrictRelativeRegex}", StrictRelativeRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{AfterNextSuffixRegex}", AfterNextSuffixRegex) + .replace("{SpecialYearPrefixes}", SpecialYearPrefixes) + .replace("{BaseDateTime.BaseAmDescRegex}", BaseDateTime.BaseAmDescRegex) + .replace("{BaseDateTime.BasePmDescRegex}", BaseDateTime.BasePmDescRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String MonthNumWithYear = "\\b(({BaseDateTime.FourDigitYearRegex}(\\s*)[/\\-\\.](\\s*){MonthNumRegex})|({MonthNumRegex}(\\s*)[/\\-](\\s*){BaseDateTime.FourDigitYearRegex}))\\b" + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{MonthNumRegex}", MonthNumRegex); + + public static final String WeekOfMonthRegex = "\\b(?(the\\s+)?(?first|1st|second|2nd|third|3rd|fourth|4th|fifth|5th|last)\\s+week\\s+{MonthSuffixRegex}(\\s+{BaseDateTime.FourDigitYearRegex}|{RelativeRegex}\\s+year)?)\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String WeekOfYearRegex = "\\b(?(the\\s+)?(?first|1st|second|2nd|third|3rd|fourth|4th|fifth|5th|last)\\s+week(\\s+of)?\\s+({YearRegex}|{RelativeRegex}\\s+year))\\b" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String FollowedDateUnit = "^\\s*{DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String NumberCombinedWithDateUnit = "\\b(?\\d+(\\.\\d*)?){DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String QuarterTermRegex = "\\b(((?first|1st|second|2nd|third|3rd|fourth|4th)[ -]+quarter)|(q(?[1-4])))\\b"; + + public static final String RelativeQuarterTermRegex = "\\b(?{StrictRelativeRegex})\\s+quarter\\b" + .replace("{StrictRelativeRegex}", StrictRelativeRegex); + + public static final String QuarterRegex = "((the\\s+)?{QuarterTermRegex}(?:((\\s+of)?\\s+|\\s*[,-]\\s*)({YearRegex}|{RelativeRegex}\\s+year))?)|{RelativeQuarterTermRegex}" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{QuarterTermRegex}", QuarterTermRegex) + .replace("{RelativeQuarterTermRegex}", RelativeQuarterTermRegex); + + public static final String QuarterRegexYearFront = "(?:{YearRegex}|{RelativeRegex}\\s+year)('s)?(?:\\s*-\\s*|\\s+(the\\s+)?)?{QuarterTermRegex}" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{QuarterTermRegex}", QuarterTermRegex); + + public static final String HalfYearTermRegex = "(?first|1st|second|2nd)\\s+half"; + + public static final String HalfYearFrontRegex = "(?((1[5-9]|20)\\d{2})|2100)(\\s*-\\s*|\\s+(the\\s+)?)?h(?[1-2])" + .replace("{YearRegex}", YearRegex); + + public static final String HalfYearBackRegex = "(the\\s+)?(h(?[1-2])|({HalfYearTermRegex}))(\\s+of|\\s*,\\s*)?\\s+({YearRegex})" + .replace("{YearRegex}", YearRegex) + .replace("{HalfYearTermRegex}", HalfYearTermRegex); + + public static final String HalfYearRelativeRegex = "(the\\s+)?{HalfYearTermRegex}(\\s+of|\\s*,\\s*)?\\s+({RelativeRegex}\\s+year)" + .replace("{RelativeRegex}", RelativeRegex) + .replace("{HalfYearTermRegex}", HalfYearTermRegex); + + public static final String AllHalfYearRegex = "({HalfYearFrontRegex})|({HalfYearBackRegex})|({HalfYearRelativeRegex})" + .replace("{HalfYearFrontRegex}", HalfYearFrontRegex) + .replace("{HalfYearBackRegex}", HalfYearBackRegex) + .replace("{HalfYearRelativeRegex}", HalfYearRelativeRegex); + + public static final String EarlyPrefixRegex = "\\b(?early|beginning of|start of|(?earlier(\\s+in)?))\\b"; + + public static final String MidPrefixRegex = "\\b(?mid-?|middle of)\\b"; + + public static final String LaterPrefixRegex = "\\b(?late|end of|(?later(\\s+in)?))\\b"; + + public static final String PrefixPeriodRegex = "({EarlyPrefixRegex}|{MidPrefixRegex}|{LaterPrefixRegex})" + .replace("{EarlyPrefixRegex}", EarlyPrefixRegex) + .replace("{MidPrefixRegex}", MidPrefixRegex) + .replace("{LaterPrefixRegex}", LaterPrefixRegex); + + public static final String PrefixDayRegex = "\\b((?early)|(?mid(dle)?)|(?later?))(\\s+in)?(\\s+the\\s+day)?$"; + + public static final String SeasonDescRegex = "(?spring|summer|fall|autumn|winter)"; + + public static final String SeasonRegex = "\\b(?({PrefixPeriodRegex}\\s+)?({RelativeRegex}\\s+)?{SeasonDescRegex}((\\s+of|\\s*,\\s*)?\\s+({YearRegex}|{RelativeRegex}\\s+year))?)\\b" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{SeasonDescRegex}", SeasonDescRegex) + .replace("{PrefixPeriodRegex}", PrefixPeriodRegex); + + public static final String WhichWeekRegex = "\\b(week)(\\s*)(?5[0-3]|[1-4]\\d|0?[1-9])\\b"; + + public static final String WeekOfRegex = "(the\\s+)?((week)(\\s+(of|(commencing|starting|beginning)(\\s+on)?))|w/c)(\\s+the)?"; + + public static final String MonthOfRegex = "(month)(\\s*)(of)"; + + public static final String DateYearRegex = "(?{BaseDateTime.FourDigitYearRegex}|(?(3[0-1]|[0-2]?\\d)(?:th|nd|rd|st))s?)\\b"; + + public static final String PrefixWeekDayRegex = "(\\s*((,?\\s*on)|[-—–]))"; + + public static final String ThisRegex = "\\b(this(\\s*week{PrefixWeekDayRegex}?)?\\s*{WeekDayRegex})|({WeekDayRegex}((\\s+of)?\\s+this\\s*week))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{PrefixWeekDayRegex}", PrefixWeekDayRegex); + + public static final String LastDateRegex = "\\b({PreviousPrefixRegex}(\\s*week{PrefixWeekDayRegex}?)?\\s*{WeekDayRegex})|({WeekDayRegex}(\\s+(of\\s+)?last\\s*week))\\b" + .replace("{PreviousPrefixRegex}", PreviousPrefixRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{PrefixWeekDayRegex}", PrefixWeekDayRegex); + + public static final String NextDateRegex = "\\b({NextPrefixRegex}(\\s*week{PrefixWeekDayRegex}?)?\\s*{WeekDayRegex})|((on\\s+)?{WeekDayRegex}((\\s+of)?\\s+(the\\s+following|(the\\s+)?next)\\s*week))\\b" + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{PrefixWeekDayRegex}", PrefixWeekDayRegex); + + public static final String SpecialDayRegex = "\\b((the\\s+)?day before yesterday|(the\\s+)?day after (tomorrow|tmr)|the\\s+day\\s+(before|after)(?!=\\s+day)|((the\\s+)?({RelativeRegex}|my)\\s+day)|yesterday|tomorrow|tmr|today|otd)\\b" + .replace("{RelativeRegex}", RelativeRegex); + + public static final String SpecialDayWithNumRegex = "\\b((?{WrittenNumRegex})\\s+days?\\s+from\\s+(?yesterday|tomorrow|tmr|today))\\b" + .replace("{WrittenNumRegex}", WrittenNumRegex); + + public static final String RelativeDayRegex = "\\b(((the\\s+)?{RelativeRegex}\\s+day))\\b" + .replace("{RelativeRegex}", RelativeRegex); + + public static final String SetWeekDayRegex = "\\b(?on\\s+)?(?morning|afternoon|evening|night|(sun|mon|tues|wednes|thurs|fri|satur)day)s\\b"; + + public static final String WeekDayOfMonthRegex = "(?(the\\s+)?(?first|1st|second|2nd|third|3rd|fourth|4th|fifth|5th|last)\\s+(week\\s+{MonthSuffixRegex}[\\.]?\\s+(on\\s+)?{WeekDayRegex}|{WeekDayRegex}\\s+{MonthSuffixRegex}))" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String RelativeWeekDayRegex = "\\b({WrittenNumRegex}\\s+{WeekDayRegex}\\s+(from\\s+now|later))\\b" + .replace("{WrittenNumRegex}", WrittenNumRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String SpecialDate = "(?=\\b(on|at)\\s+the\\s+){DayRegex}\\b" + .replace("{DayRegex}", DayRegex); + + public static final String DatePreposition = "\\b(on|in)"; + + public static final String DateExtractorYearTermRegex = "(\\s+|\\s*[/\\\\.,-]\\s*|\\s+of\\s+){DateYearRegex}" + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DayPrefix = "\\b({WeekDayRegex}|{SpecialDayRegex})\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{SpecialDayRegex}", SpecialDayRegex); + + public static final String DateExtractor1 = "\\b({DayPrefix}\\s*[,-]?\\s*)?(({MonthRegex}[\\.]?\\s*[/\\\\.,-]?\\s*{DayRegex})|(\\({MonthRegex}\\s*[-./]\\s*{DayRegex}\\)))(\\s*\\(\\s*{DayPrefix}\\s*\\))?({DateExtractorYearTermRegex}\\b)?" + .replace("{DayPrefix}", DayPrefix) + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateExtractorYearTermRegex}", DateExtractorYearTermRegex); + + public static final String DateExtractor3 = "\\b({DayPrefix}(\\s+|\\s*,\\s*))?({DayRegex}[\\.]?(\\s+|\\s*[-,/]\\s*|\\s+of\\s+){MonthRegex}[\\.]?((\\s+in)?{DateExtractorYearTermRegex})?|{BaseDateTime.FourDigitYearRegex}\\s*[-./]?\\s*(the\\s+)?(?(?:3[0-1]|[1-2]\\d|0?[1-9])(?:th|nd|rd|st)?)[\\.]?(\\s+|\\s*[-,/]\\s*|\\s+of\\s+){MonthRegex}[\\.]?)\\b" + .replace("{DayPrefix}", DayPrefix) + .replace("{DayRegex}", DayRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DateExtractorYearTermRegex}", DateExtractorYearTermRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex); + + public static final String DateExtractor4 = "\\b{MonthNumRegex}\\s*[/\\\\\\-]\\s*{DayRegex}[\\.]?\\s*[/\\\\\\-]\\s*{DateYearRegex}" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DateExtractor5 = "\\b({DayPrefix}(\\s*,)?\\s+)?{DayRegex}\\s*[/\\\\\\-\\.]\\s*({MonthNumRegex}|{MonthRegex})\\s*[/\\\\\\-\\.]\\s*{DateYearRegex}(?!\\s*[/\\\\\\-\\.]\\s*\\d+)" + .replace("{DayPrefix}", DayPrefix) + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DateExtractor6 = "(?<={DatePreposition}\\s+)({StrictRelativeRegex}\\s+)?({DayPrefix}\\s+)?{MonthNumRegex}[\\-\\.]{DayRegex}(?![%]){BaseDateTime.CheckDecimalRegex}\\b" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DayPrefix}", DayPrefix) + .replace("{DatePreposition}", DatePreposition) + .replace("{StrictRelativeRegex}", StrictRelativeRegex) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor7L = "\\b({DayPrefix}(\\s*,)?\\s+)?{MonthNumRegex}\\s*/\\s*{DayRegex}{DateExtractorYearTermRegex}(?![%])\\b" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DayPrefix}", DayPrefix) + .replace("{DateExtractorYearTermRegex}", DateExtractorYearTermRegex); + + public static final String DateExtractor7S = "\\b({DayPrefix}(\\s*,)?\\s+)?{MonthNumRegex}\\s*/\\s*{DayRegex}(?![%]){BaseDateTime.CheckDecimalRegex}\\b" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DayPrefix}", DayPrefix) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor8 = "(?<={DatePreposition}\\s+)({StrictRelativeRegex}\\s+)?({DayPrefix}\\s+)?{DayRegex}[\\\\\\-]{MonthNumRegex}(?![%]){BaseDateTime.CheckDecimalRegex}\\b" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayPrefix}", DayPrefix) + .replace("{DatePreposition}", DatePreposition) + .replace("{StrictRelativeRegex}", StrictRelativeRegex) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor9L = "\\b({DayPrefix}(\\s*,)?\\s+)?{DayRegex}\\s*/\\s*{MonthNumRegex}{DateExtractorYearTermRegex}(?![%])\\b" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayPrefix}", DayPrefix) + .replace("{DateExtractorYearTermRegex}", DateExtractorYearTermRegex); + + public static final String DateExtractor9S = "\\b({DayPrefix}(\\s*,)?\\s+)?{DayRegex}\\s*/\\s*{MonthNumRegex}{BaseDateTime.CheckDecimalRegex}(?![%])\\b" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayPrefix}", DayPrefix) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractorA = "\\b({DayPrefix}(\\s*,)?\\s+)?(({BaseDateTime.FourDigitYearRegex}\\s*[/\\\\\\-\\.]\\s*({MonthNumRegex}|{MonthRegex})\\s*[/\\\\\\-\\.]\\s*{DayRegex})|({MonthRegex}\\s*[/\\\\\\-\\.]\\s*{BaseDateTime.FourDigitYearRegex}\\s*[/\\\\\\-\\.]\\s*(the\\s+)?(?(?:3[0-1]|[1-2]\\d|0?[1-9])(?:th|nd|rd|st)?))|({DayRegex}\\s*[/\\\\\\-\\.]\\s*{BaseDateTime.FourDigitYearRegex}\\s*[/\\\\\\-\\.]\\s*{MonthRegex}))" + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DayPrefix}", DayPrefix); + + public static final String OfMonth = "^\\s*(day\\s+)?of\\s*{MonthRegex}" + .replace("{MonthRegex}", MonthRegex); + + public static final String MonthEnd = "{MonthRegex}\\s*(the)?\\s*$" + .replace("{MonthRegex}", MonthRegex); + + public static final String WeekDayEnd = "(this\\s+)?{WeekDayRegex}\\s*,?\\s*$" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String WeekDayStart = "^[\\.]"; + + public static final String RangeUnitRegex = "\\b(?years?|months?|weeks?)\\b"; + + public static final String HourNumRegex = "\\b(?zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\\b"; + + public static final String MinuteNumRegex = "(?ten|eleven|twelve|thirteen|fifteen|eighteen|(four|six|seven|nine)(teen)?|twenty|thirty|forty|fifty|one|two|three|five|eight)"; + + public static final String DeltaMinuteNumRegex = "(?ten|eleven|twelve|thirteen|fifteen|eighteen|(four|six|seven|nine)(teen)?|twenty|thirty|forty|fifty|one|two|three|five|eight)"; + + public static final String PmRegex = "(?(((?:at|in|around|circa|on|for)\\s+(the\\s+)?)?(afternoon|evening|midnight|lunchtime))|((at|in|around|on|for)\\s+(the\\s+)?night))"; + + public static final String PmRegexFull = "(?((?:at|in|around|circa|on|for)\\s+(the\\s+)?)?(afternoon|evening|(mid)?night|lunchtime))"; + + public static final String AmRegex = "(?((?:at|in|around|circa|on|for)\\s+(the\\s+)?)?(morning))"; + + public static final String LunchRegex = "\\blunchtime\\b"; + + public static final String NightRegex = "\\b(mid)?night\\b"; + + public static final String CommonDatePrefixRegex = "^[\\.]"; + + public static final String LessThanOneHour = "(?(a\\s+)?quarter|three quarter(s)?|half( an hour)?|{BaseDateTime.DeltaMinuteRegex}(\\s+(minutes?|mins?))|{DeltaMinuteNumRegex}(\\s+(minutes?|mins?)))" + .replace("{BaseDateTime.DeltaMinuteRegex}", BaseDateTime.DeltaMinuteRegex) + .replace("{DeltaMinuteNumRegex}", DeltaMinuteNumRegex); + + public static final String WrittenTimeRegex = "(?{HourNumRegex}\\s+({MinuteNumRegex}|(?twenty|thirty|fou?rty|fifty)\\s+{MinuteNumRegex}))" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex); + + public static final String TimePrefix = "(?{LessThanOneHour}\\s+(past|to))" + .replace("{LessThanOneHour}", LessThanOneHour); + + public static final String TimeSuffix = "(?{AmRegex}|{PmRegex}|{OclockRegex})" + .replace("{AmRegex}", AmRegex) + .replace("{PmRegex}", PmRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String TimeSuffixFull = "(?{AmRegex}|{PmRegexFull}|{OclockRegex})" + .replace("{AmRegex}", AmRegex) + .replace("{PmRegexFull}", PmRegexFull) + .replace("{OclockRegex}", OclockRegex); + + public static final String BasicTime = "\\b(?{WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}:{BaseDateTime.MinuteRegex}(:{BaseDateTime.SecondRegex})?|{BaseDateTime.HourRegex}(?![%\\d]))" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex); + + public static final String MidnightRegex = "(?mid\\s*(-\\s*)?night)"; + + public static final String MidmorningRegex = "(?mid\\s*(-\\s*)?morning)"; + + public static final String MidafternoonRegex = "(?mid\\s*(-\\s*)?afternoon)"; + + public static final String MiddayRegex = "(?mid\\s*(-\\s*)?day|((12\\s)?noon))"; + + public static final String MidTimeRegex = "(?({MidnightRegex}|{MidmorningRegex}|{MidafternoonRegex}|{MiddayRegex}))" + .replace("{MidnightRegex}", MidnightRegex) + .replace("{MidmorningRegex}", MidmorningRegex) + .replace("{MidafternoonRegex}", MidafternoonRegex) + .replace("{MiddayRegex}", MiddayRegex); + + public static final String AtRegex = "\\b(?:(?:(?<=\\b(at|(at)?\\s*around|circa)\\s+)(?:{WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}(?!\\.\\d)(\\s*((?a)|(?p)))?|{MidTimeRegex}))|{MidTimeRegex})\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{MidTimeRegex}", MidTimeRegex); + + public static final String IshRegex = "\\b({BaseDateTime.HourRegex}(-|——)?ish|noon(ish)?)\\b" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String TimeUnitRegex = "([^A-Za-z]{1,}|\\b)(?h(ou)?rs?|h|min(ute)?s?|sec(ond)?s?)\\b"; + + public static final String RestrictedTimeUnitRegex = "(?hour|minute)\\b"; + + public static final String FivesRegex = "(?(?:fifteen|(?:twen|thir|fou?r|fif)ty(\\s*five)?|ten|five))\\b"; + + public static final String HourRegex = "\\b{BaseDateTime.HourRegex}" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String PeriodHourNumRegex = "\\b(?twenty(\\s+(one|two|three|four))?|eleven|twelve|thirteen|fifteen|eighteen|(four|six|seven|nine)(teen)?|zero|one|two|three|five|eight|ten)\\b"; + + public static final String ConnectNumRegex = "\\b{BaseDateTime.HourRegex}(?[0-5][0-9])\\s*{DescRegex}" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegexWithDotConnector = "({BaseDateTime.HourRegex}(\\s*\\.\\s*){BaseDateTime.MinuteRegex})" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex); + + public static final String TimeRegex1 = "\\b({TimePrefix}\\s+)?({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})(\\s*|[.]){DescRegex}" + .replace("{TimePrefix}", TimePrefix) + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex2 = "(\\b{TimePrefix}\\s+)?(t)?{BaseDateTime.HourRegex}(\\s*)?:(\\s*)?{BaseDateTime.MinuteRegex}((\\s*)?:(\\s*)?{BaseDateTime.SecondRegex})?(?a)?((\\s*{DescRegex})|\\b)" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex3 = "(\\b{TimePrefix}\\s+)?{BaseDateTime.HourRegex}\\.{BaseDateTime.MinuteRegex}(\\s*{DescRegex})" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex4 = "\\b{TimePrefix}\\s+{BasicTime}(\\s*{DescRegex})?\\s+{TimeSuffix}\\b" + .replace("{TimePrefix}", TimePrefix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex5 = "\\b{TimePrefix}\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimePrefix}", TimePrefix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex6 = "({BasicTime})(\\s*{DescRegex})?\\s+{TimeSuffix}\\b" + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex7 = "\\b{TimeSuffixFull}\\s+(at\\s+)?{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffixFull}", TimeSuffixFull) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex8 = ".^" + .replace("{TimeSuffixFull}", TimeSuffixFull) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex9 = "\\b{PeriodHourNumRegex}(\\s+|-){FivesRegex}((\\s*{DescRegex})|\\b)" + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{FivesRegex}", FivesRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex10 = "\\b({TimePrefix}\\s+)?{BaseDateTime.HourRegex}(\\s*h\\s*){BaseDateTime.MinuteRegex}(\\s*{DescRegex})?" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex11 = "\\b((?:({TimeTokenPrefix})?{TimeRegexWithDotConnector}(\\s*{DescRegex}))|(?:(?:{TimeTokenPrefix}{TimeRegexWithDotConnector})(?!\\s*per\\s*cent|%)))" + .replace("{TimeTokenPrefix}", TimeTokenPrefix) + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{DescRegex}", DescRegex); + + public static final String FirstTimeRegexInTimeRange = "\\b{TimeRegexWithDotConnector}(\\s*{DescRegex})?" + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{DescRegex}", DescRegex); + + public static final String PureNumFromTo = "({RangePrefixRegex}\\s+)?({HourRegex}|{PeriodHourNumRegex})(\\s*(?{DescRegex}))?\\s*{TillRegex}\\s*({HourRegex}|{PeriodHourNumRegex})(?\\s*({PmRegex}|{AmRegex}|{DescRegex}))?" + .replace("{HourRegex}", HourRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{TillRegex}", TillRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex) + .replace("{RangePrefixRegex}", RangePrefixRegex); + + public static final String PureNumBetweenAnd = "(between\\s+)(({BaseDateTime.TwoDigitHourRegex}{BaseDateTime.TwoDigitMinuteRegex})|{HourRegex}|{PeriodHourNumRegex})(\\s*(?{DescRegex}))?\\s*{RangeConnectorRegex}\\s*(({BaseDateTime.TwoDigitHourRegex}{BaseDateTime.TwoDigitMinuteRegex})|{HourRegex}|{PeriodHourNumRegex})(?\\s*({PmRegex}|{AmRegex}|{DescRegex}))?" + .replace("{HourRegex}", HourRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{BaseDateTime.TwoDigitHourRegex}", BaseDateTime.TwoDigitHourRegex) + .replace("{BaseDateTime.TwoDigitMinuteRegex}", BaseDateTime.TwoDigitMinuteRegex) + .replace("{DescRegex}", DescRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex); + + public static final String SpecificTimeFromTo = "({RangePrefixRegex}\\s+)?(?(({TimeRegex2}|{FirstTimeRegexInTimeRange})|({HourRegex}|{PeriodHourNumRegex})(\\s*(?{DescRegex}))?))\\s*{TillRegex}\\s*(?(({TimeRegex2}|{TimeRegexWithDotConnector}(?\\s*{DescRegex}))|({HourRegex}|{PeriodHourNumRegex})(\\s*(?{DescRegex}))?))" + .replace("{TimeRegex2}", TimeRegex2) + .replace("{FirstTimeRegexInTimeRange}", FirstTimeRegexInTimeRange) + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{TillRegex}", TillRegex) + .replace("{HourRegex}", HourRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex) + .replace("{RangePrefixRegex}", RangePrefixRegex); + + public static final String SpecificTimeBetweenAnd = "(between\\s+)(?(({TimeRegex2}|{FirstTimeRegexInTimeRange})|({HourRegex}|{PeriodHourNumRegex})(\\s*(?{DescRegex}))?))\\s*{RangeConnectorRegex}\\s*(?(({TimeRegex2}|{TimeRegexWithDotConnector}(?\\s*{DescRegex}))|({HourRegex}|{PeriodHourNumRegex})(\\s*(?{DescRegex}))?))" + .replace("{TimeRegex2}", TimeRegex2) + .replace("{FirstTimeRegexInTimeRange}", FirstTimeRegexInTimeRange) + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{HourRegex}", HourRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex); + + public static final String SuffixAfterRegex = "\\b(((at)\\s)?(or|and)\\s+(above|after|later|greater)(?!\\s+than))\\b"; + + public static final String PrepositionRegex = "(?^(,\\s*)?(at|on|of)(\\s+the)?$)"; + + public static final String LaterEarlyRegex = "((?early(\\s+|-))|(?late(r?\\s+|-)))"; + + public static final String MealTimeRegex = "\\b(at\\s+)?(?breakfast|brunch|lunch(\\s*time)?|dinner(\\s*time)?|supper)\\b"; + + public static final String UnspecificTimePeriodRegex = "({MealTimeRegex})" + .replace("{MealTimeRegex}", MealTimeRegex); + + public static final String TimeOfDayRegex = "\\b(?((((in\\s+the\\s+)?{LaterEarlyRegex}?(in(\\s+the)?\\s+)?(morning|afternoon|night|evening)))|{MealTimeRegex}|(((in\\s+(the)?\\s+)?)(daytime|business\\s+hour)))s?)\\b" + .replace("{LaterEarlyRegex}", LaterEarlyRegex) + .replace("{MealTimeRegex}", MealTimeRegex); + + public static final String SpecificTimeOfDayRegex = "\\b(({StrictRelativeRegex}\\s+{TimeOfDayRegex})\\b|\\btoni(ght|te))s?\\b" + .replace("{TimeOfDayRegex}", TimeOfDayRegex) + .replace("{StrictRelativeRegex}", StrictRelativeRegex); + + public static final String TimeFollowedUnit = "^\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String TimeNumberCombinedWithUnit = "\\b(?\\d+(\\.\\d*)?){TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final List BusinessHourSplitStrings = Arrays.asList("business", "hour"); + + public static final String NowRegex = "\\b(?(right\\s+)?now|at th(e|is) minute|as soon as possible|asap|recently|previously)\\b"; + + public static final String NowParseRegex = "\\b({NowRegex}|^(date)$)\\b" + .replace("{NowRegex}", NowRegex); + + public static final String SuffixRegex = "^\\s*(in the\\s+)?(morning|afternoon|evening|night)\\b"; + + public static final String NonTimeContextTokens = "(building)"; + + public static final String DateTimeTimeOfDayRegex = "\\b(?morning|(?afternoon|night|evening))\\b"; + + public static final String DateTimeSpecificTimeOfDayRegex = "\\b(({RelativeRegex}\\s+{DateTimeTimeOfDayRegex})\\b|\\btoni(ght|te))\\b" + .replace("{DateTimeTimeOfDayRegex}", DateTimeTimeOfDayRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String TimeOfTodayAfterRegex = "^\\s*(,\\s*)?(in\\s+)?{DateTimeSpecificTimeOfDayRegex}" + .replace("{DateTimeSpecificTimeOfDayRegex}", DateTimeSpecificTimeOfDayRegex); + + public static final String TimeOfTodayBeforeRegex = "{DateTimeSpecificTimeOfDayRegex}(\\s*,)?(\\s+(at|around|circa|in|on))?\\s*$" + .replace("{DateTimeSpecificTimeOfDayRegex}", DateTimeSpecificTimeOfDayRegex); + + public static final String SimpleTimeOfTodayAfterRegex = "(?{DateUnitRegex}|h(ou)?rs?|h|min(ute)?s?|sec(ond)?s?|nights?)\\b" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String SuffixAndRegex = "(?\\s*(and)\\s+(an?\\s+)?(?half|quarter))"; + + public static final String PeriodicRegex = "\\b(?((?semi|bi|tri)(\\s*|-))?(daily|monthly|weekly|quarterly|yearly|annual(ly)?))\\b"; + + public static final String EachUnitRegex = "\\b(?(each|every|any|once an?)(?\\s+other)?\\s+({DurationUnitRegex}|(?quarters?|weekends?)|{WeekDayRegex})|(?weekends))" + .replace("{DurationUnitRegex}", DurationUnitRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String EachPrefixRegex = "\\b(?(each|every|once an?)\\s*$)"; + + public static final String SetEachRegex = "\\b(?(each|every)(?\\s+other)?\\s*)(?!the|that)\\b"; + + public static final String SetLastRegex = "(?following|next|upcoming|this|{LastNegPrefix}last|past|previous|current)" + .replace("{LastNegPrefix}", LastNegPrefix); + + public static final String EachDayRegex = "^\\s*(each|every)\\s*day\\b"; + + public static final String DurationFollowedUnit = "(^\\s*{DurationUnitRegex}\\s+{SuffixAndRegex})|(^\\s*{SuffixAndRegex}?(\\s+|-)?{DurationUnitRegex})" + .replace("{SuffixAndRegex}", SuffixAndRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String NumberCombinedWithDurationUnit = "\\b(?\\d+(\\.\\d*)?)(-)?{DurationUnitRegex}" + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String AnUnitRegex = "(\\b((?(half)\\s+)?an?|another)|(?(1/2|½|half)))\\s+{DurationUnitRegex}" + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String DuringRegex = "\\b(for|during)\\s+the\\s+(?year|month|week|day)\\b"; + + public static final String AllRegex = "\\b(?(all|full|whole)(\\s+|-)(?year|month|week|day))\\b"; + + public static final String HalfRegex = "((an?\\s*)|\\b)(?half\\s+(?year|month|week|day|hour))\\b"; + + public static final String ConjunctionRegex = "\\b((and(\\s+for)?)|with)\\b"; + + public static final String HolidayList1 = "(?mardi gras|(washington|mao)'s birthday|juneteenth|(jubilee|freedom)(\\s+day)|chinese new year|(new\\s+(years'|year\\s*'s|years?)\\s+eve)|(new\\s+(years'|year\\s*'s|years?)(\\s+day)?)|may\\s*day|yuan dan|christmas eve|(christmas|xmas)(\\s+day)?|black friday|yuandan|easter(\\s+(sunday|saturday|monday))?|clean monday|ash wednesday|palm sunday|maundy thursday|good friday|white\\s+(sunday|monday)|trinity sunday|pentecost|corpus christi|cyber monday)"; + + public static final String HolidayList2 = "(?(thanks\\s*giving|all saint's|white lover|s(?:ain)?t?(\\.)?\\s+(?:patrick|george)(?:')?(?:s)?|us independence|all hallow|all souls|guy fawkes|cinco de mayo|halloween|qingming|dragon boat|april fools|tomb\\s*sweeping)(\\s+day)?)"; + + public static final String HolidayList3 = "(?(?:independence|presidents(?:')?|mlk|martin luther king( jr)?|canberra|ascension|columbus|tree( planting)?|arbor|labou?r|((international|int'?l)\\s+)?workers'?|mother'?s?|father'?s?|female|women('s)?|single|teacher'?s|youth|children|girls|lovers?|earth|inauguration|groundhog|valentine'?s|baptiste|bastille|veterans(?:')?|memorial|mid[ \\-]autumn|moon|spring|lantern)\\s+day)"; + + public static final String HolidayRegex = "\\b(({StrictRelativeRegex}\\s+({HolidayList1}|{HolidayList2}|{HolidayList3}))|(({HolidayList1}|{HolidayList2}|{HolidayList3})(\\s+(of\\s+)?({YearRegex}|{RelativeRegex}\\s+year))?))\\b" + .replace("{HolidayList1}", HolidayList1) + .replace("{HolidayList2}", HolidayList2) + .replace("{HolidayList3}", HolidayList3) + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{StrictRelativeRegex}", StrictRelativeRegex); + + public static final String AMTimeRegex = "(?morning)"; + + public static final String PMTimeRegex = "\\b(?afternoon|evening|night)\\b"; + + public static final String NightTimeRegex = "(night)"; + + public static final String NowTimeRegex = "(now|at th(e|is) minute)"; + + public static final String RecentlyTimeRegex = "(recently|previously)"; + + public static final String AsapTimeRegex = "(as soon as possible|asap)"; + + public static final String InclusiveModPrepositions = "(?((on|in|at)\\s+or\\s+)|(\\s+or\\s+(on|in|at)))"; + + public static final String AroundRegex = "(?:\\b(?:around|circa)\\s*?\\b)(\\s+the)?"; + + public static final String BeforeRegex = "((\\b{InclusiveModPrepositions}?(?:before|in\\s+advance\\s+of|prior\\s+to|(no\\s+later|earlier|sooner)\\s+than|ending\\s+(with|on)|by|(un)?till?|(?as\\s+late\\s+as)){InclusiveModPrepositions}?\\b\\s*?)|(?)((?<\\s*=)|<))(\\s+the)?" + .replace("{InclusiveModPrepositions}", InclusiveModPrepositions); + + public static final String AfterRegex = "((\\b{InclusiveModPrepositions}?((after|(starting|beginning)(\\s+on)?(?!\\sfrom)|(?>\\s*=)|>))(\\s+the)?" + .replace("{InclusiveModPrepositions}", InclusiveModPrepositions); + + public static final String SinceRegex = "(?:(?:\\b(?:since|after\\s+or\\s+equal\\s+to|starting\\s+(?:from|on|with)|as\\s+early\\s+as|(any\\s+time\\s+)from)\\b\\s*?)|(?=))(\\s+the)?"; + + public static final String SinceRegexExp = "({SinceRegex}|\\bfrom(\\s+the)?\\b)" + .replace("{SinceRegex}", SinceRegex); + + public static final String AgoRegex = "\\b(ago|before\\s+(?yesterday|today))\\b"; + + public static final String LaterRegex = "\\b(?:later(?!((\\s+in)?\\s*{OneWordPeriodRegex})|(\\s+{TimeOfDayRegex})|\\s+than\\b)|from now|(from|after)\\s+(?tomorrow|tmr|today))\\b" + .replace("{OneWordPeriodRegex}", OneWordPeriodRegex) + .replace("{TimeOfDayRegex}", TimeOfDayRegex); + + public static final String BeforeAfterRegex = "\\b((?before)|(?from|after))\\b"; + + public static final String InConnectorRegex = "\\b(in)\\b"; + + public static final String SinceYearSuffixRegex = "(^\\s*{SinceRegex}(\\s*(the\\s+)?year\\s*)?{YearSuffix})" + .replace("{SinceRegex}", SinceRegex) + .replace("{YearSuffix}", YearSuffix); + + public static final String WithinNextPrefixRegex = "\\b(within(\\s+the)?(\\s+(?{NextPrefixRegex}))?)\\b" + .replace("{NextPrefixRegex}", NextPrefixRegex); + + public static final String TodayNowRegex = "\\b(today|now)\\b"; + + public static final String MorningStartEndRegex = "(^(morning|{AmDescRegex}))|((morning|{AmDescRegex})$)" + .replace("{AmDescRegex}", AmDescRegex); + + public static final String AfternoonStartEndRegex = "(^(afternoon|{PmDescRegex}))|((afternoon|{PmDescRegex})$)" + .replace("{PmDescRegex}", PmDescRegex); + + public static final String EveningStartEndRegex = "(^(evening))|((evening)$)"; + + public static final String NightStartEndRegex = "(^(over|to)?ni(ght|te))|((over|to)?ni(ght|te)$)"; + + public static final String InexactNumberRegex = "\\b((a\\s+)?few|some|several|(?(a\\s+)?couple(\\s+of)?))\\b"; + + public static final String InexactNumberUnitRegex = "({InexactNumberRegex})\\s+({DurationUnitRegex})" + .replace("{InexactNumberRegex}", InexactNumberRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String RelativeTimeUnitRegex = "(?:(?:(?:{NextPrefixRegex}|{PreviousPrefixRegex}|{ThisPrefixRegex})\\s+({TimeUnitRegex}))|((the|my))\\s+({RestrictedTimeUnitRegex}))" + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{PreviousPrefixRegex}", PreviousPrefixRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{TimeUnitRegex}", TimeUnitRegex) + .replace("{RestrictedTimeUnitRegex}", RestrictedTimeUnitRegex); + + public static final String RelativeDurationUnitRegex = "(?:(?:(?<=({NextPrefixRegex}|{PreviousPrefixRegex}|{ThisPrefixRegex})\\s+)({DurationUnitRegex}))|((the|my))\\s+({RestrictedTimeUnitRegex}))" + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{PreviousPrefixRegex}", PreviousPrefixRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex) + .replace("{RestrictedTimeUnitRegex}", RestrictedTimeUnitRegex); + + public static final String ReferenceDatePeriodRegex = "\\b{ReferencePrefixRegex}\\s+(?week|month|year|decade|weekend)\\b" + .replace("{ReferencePrefixRegex}", ReferencePrefixRegex); + + public static final String ConnectorRegex = "^(-|,|for|t|around|circa|@)$"; + + public static final String FromToRegex = "(\\b(from).+(to|and|or)\\b.+)"; + + public static final String SingleAmbiguousMonthRegex = "^(the\\s+)?(may|march)$"; + + public static final String SingleAmbiguousTermsRegex = "^(the\\s+)?(day|week|month|year)$"; + + public static final String UnspecificDatePeriodRegex = "^(week|month|year)$"; + + public static final String PrepositionSuffixRegex = "\\b(on|in|at|around|circa|from|to)$"; + + public static final String FlexibleDayRegex = "(?([A-Za-z]+\\s)?[A-Za-z\\d]+)"; + + public static final String ForTheRegex = "\\b((((?<=for\\s+)the\\s+{FlexibleDayRegex})|((?<=on\\s+)(the\\s+)?{FlexibleDayRegex}(?<=(st|nd|rd|th))))(?\\s*(,|\\.(?!\\d)|!|\\?|$)))" + .replace("{FlexibleDayRegex}", FlexibleDayRegex); + + public static final String WeekDayAndDayOfMonthRegex = "\\b{WeekDayRegex}\\s+(the\\s+{FlexibleDayRegex})\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{FlexibleDayRegex}", FlexibleDayRegex); + + public static final String WeekDayAndDayRegex = "\\b{WeekDayRegex}\\s+(?!(the)){DayRegex}(?!([-:]|(\\s+({AmDescRegex}|{PmDescRegex}|{OclockRegex}))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DayRegex}", DayRegex) + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String RestOfDateRegex = "\\b(rest|remaining)\\s+(of\\s+)?((the|my|this|current)\\s+)?(?week|month|year|decade)\\b"; + + public static final String RestOfDateTimeRegex = "\\b(rest|remaining)\\s+(of\\s+)?((the|my|this|current)\\s+)?(?day)\\b"; + + public static final String AmbiguousRangeModifierPrefix = "(from)"; + + public static final String NumberEndingPattern = "^(?:\\s+(?meeting|appointment|conference|((skype|teams|zoom|facetime)\\s+)?call)\\s+to\\s+(?{PeriodHourNumRegex}|{HourRegex})([\\.]?$|(\\.,|,|!|\\?)))" + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{HourRegex}", HourRegex); + + public static final String OneOnOneRegex = "\\b(1\\s*:\\s*1(?!\\d))|(one (on )?one|one\\s*-\\s*one|one\\s*:\\s*one)\\b"; + + public static final String LaterEarlyPeriodRegex = "\\b(({PrefixPeriodRegex})\\s*\\b\\s*(?{OneWordPeriodRegex}|(?{BaseDateTime.FourDigitYearRegex}))|({UnspecificEndOfRangeRegex}))\\b" + .replace("{PrefixPeriodRegex}", PrefixPeriodRegex) + .replace("{OneWordPeriodRegex}", OneWordPeriodRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{UnspecificEndOfRangeRegex}", UnspecificEndOfRangeRegex); + + public static final String WeekWithWeekDayRangeRegex = "\\b((?({NextPrefixRegex}|{PreviousPrefixRegex}|this)\\s+week)((\\s+between\\s+{WeekDayRegex}\\s+and\\s+{WeekDayRegex})|(\\s+from\\s+{WeekDayRegex}\\s+to\\s+{WeekDayRegex})))\\b" + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{PreviousPrefixRegex}", PreviousPrefixRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String GeneralEndingRegex = "^\\s*((\\.,)|\\.|,|!|\\?)?\\s*$"; + + public static final String MiddlePauseRegex = "\\s*(,)\\s*"; + + public static final String DurationConnectorRegex = "^\\s*(?\\s+|and|,)\\s*$"; + + public static final String PrefixArticleRegex = "\\bthe\\s+"; + + public static final String OrRegex = "\\s*((\\b|,\\s*)(or|and)\\b|,)\\s*"; + + public static final String SpecialYearTermsRegex = "\\b((({SpecialYearPrefixes}\\s+)?year)|(cy|(?fy|sy)))" + .replace("{SpecialYearPrefixes}", SpecialYearPrefixes); + + public static final String YearPlusNumberRegex = "\\b({SpecialYearTermsRegex}\\s*((?(\\d{2,4}))|{FullTextYearRegex}))\\b" + .replace("{FullTextYearRegex}", FullTextYearRegex) + .replace("{SpecialYearTermsRegex}", SpecialYearTermsRegex); + + public static final String NumberAsTimeRegex = "\\b({WrittenTimeRegex}|{PeriodHourNumRegex}|{BaseDateTime.HourRegex})\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String TimeBeforeAfterRegex = "\\b(((?<=\\b(before|no later than|by|after)\\s+)({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}|{MidTimeRegex}))|{MidTimeRegex})\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{MidTimeRegex}", MidTimeRegex); + + public static final String DateNumberConnectorRegex = "^\\s*(?\\s+at)\\s*$"; + + public static final String DecadeRegex = "(?(?:nough|twen|thir|fou?r|fif|six|seven|eigh|nine)ties|two\\s+thousands)"; + + public static final String DecadeWithCenturyRegex = "(the\\s+)?(((?\\d|1\\d|2\\d)?(')?(?\\d0)(')?(\\s)?s\\b)|(({CenturyRegex}(\\s+|-)(and\\s+)?)?{DecadeRegex})|({CenturyRegex}(\\s+|-)(and\\s+)?(?tens|hundreds)))" + .replace("{CenturyRegex}", CenturyRegex) + .replace("{DecadeRegex}", DecadeRegex); + + public static final String RelativeDecadeRegex = "\\b((the\\s+)?{RelativeRegex}\\s+((?[\\w,]+)\\s+)?decades?)\\b" + .replace("{RelativeRegex}", RelativeRegex); + + public static final String YearPeriodRegex = "((((from|during|in)\\s+)?{YearRegex}\\s*({TillRegex})\\s*{YearRegex})|(((between)\\s+){YearRegex}\\s*({RangeConnectorRegex})\\s*{YearRegex}))" + .replace("{YearRegex}", YearRegex) + .replace("{TillRegex}", TillRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex); + + public static final String StrictTillRegex = "(?\\b(to|(un)?till?|thru|through)\\b|{BaseDateTime.RangeConnectorSymbolRegex}(?!\\s*(h[1-2]|q[1-4])(?!(\\s+of|\\s*,\\s*))))" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String StrictRangeConnectorRegex = "(?\\b(and|through|to)\\b|{BaseDateTime.RangeConnectorSymbolRegex}(?!\\s*(h[1-2]|q[1-4])(?!(\\s+of|\\s*,\\s*))))" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String StartMiddleEndRegex = "\\b((?((the\\s+)?(start|beginning)\\s+of\\s+)?)(?((the\\s+)?middle\\s+of\\s+)?)(?((the\\s+)?end\\s+of\\s+)?))"; + + public static final String ComplexDatePeriodRegex = "(?:((from|during|in)\\s+)?{StartMiddleEndRegex}(?.+)\\s*({StrictTillRegex})\\s*{StartMiddleEndRegex}(?.+)|((between)\\s+){StartMiddleEndRegex}(?.+)\\s*({StrictRangeConnectorRegex})\\s*{StartMiddleEndRegex}(?.+))" + .replace("{StrictTillRegex}", StrictTillRegex) + .replace("{StrictRangeConnectorRegex}", StrictRangeConnectorRegex) + .replace("{StartMiddleEndRegex}", StartMiddleEndRegex); + + public static final String FailFastRegex = "{BaseDateTime.DeltaMinuteRegex}|\\b(?:{BaseDateTime.BaseAmDescRegex}|{BaseDateTime.BasePmDescRegex})|{BaseDateTime.BaseAmPmDescRegex}|\\b(?:zero|{WrittenOneToNineRegex}|{WrittenElevenToNineteenRegex}|{WrittenTensRegex}|{WrittenMonthRegex}|{SeasonDescRegex}|{DecadeRegex}|centur(y|ies)|weekends?|quarters?|hal(f|ves)|yesterday|to(morrow|day|night)|tmr|noonish|\\d(-|——)?ish|((the\\s+\\w*)|\\d)(th|rd|nd|st)|(mid\\s*(-\\s*)?)?(night|morning|afternoon|day)s?|evenings?||noon|lunch(time)?|dinner(time)?|(day|night)time|overnight|dawn|dusk|sunset|hours?|hrs?|h|minutes?|mins?|seconds?|secs?|eo[dmy]|mardi[ -]?gras|birthday|eve|christmas|xmas|thanksgiving|halloween|yuandan|easter|yuan dan|april fools|cinco de mayo|all (hallow|souls)|guy fawkes|(st )?patrick|hundreds?|noughties|aughts|thousands?)\\b|{WeekDayRegex}|{SetWeekDayRegex}|{NowRegex}|{PeriodicRegex}|\\b({DateUnitRegex}|{ImplicitDayRegex})" + .replace("{BaseDateTime.DeltaMinuteRegex}", BaseDateTime.DeltaMinuteRegex) + .replace("{BaseDateTime.BaseAmDescRegex}", BaseDateTime.BaseAmDescRegex) + .replace("{BaseDateTime.BasePmDescRegex}", BaseDateTime.BasePmDescRegex) + .replace("{BaseDateTime.BaseAmPmDescRegex}", BaseDateTime.BaseAmPmDescRegex) + .replace("{ImplicitDayRegex}", ImplicitDayRegex) + .replace("{DateUnitRegex}", DateUnitRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{SetWeekDayRegex}", SetWeekDayRegex) + .replace("{NowRegex}", NowRegex) + .replace("{PeriodicRegex}", PeriodicRegex) + .replace("{DecadeRegex}", DecadeRegex) + .replace("{SeasonDescRegex}", SeasonDescRegex) + .replace("{WrittenMonthRegex}", WrittenMonthRegex) + .replace("{WrittenTensRegex}", WrittenTensRegex) + .replace("{WrittenElevenToNineteenRegex}", WrittenElevenToNineteenRegex) + .replace("{WrittenOneToNineRegex}", WrittenOneToNineRegex); + + public static final ImmutableMap UnitMap = ImmutableMap.builder() + .put("decades", "10Y") + .put("decade", "10Y") + .put("years", "Y") + .put("year", "Y") + .put("months", "MON") + .put("month", "MON") + .put("quarters", "3MON") + .put("quarter", "3MON") + .put("semesters", "6MON") + .put("semestres", "6MON") + .put("semester", "6MON") + .put("semestre", "6MON") + .put("weeks", "W") + .put("week", "W") + .put("weekends", "WE") + .put("weekend", "WE") + .put("fortnights", "2W") + .put("fortnight", "2W") + .put("weekdays", "D") + .put("weekday", "D") + .put("days", "D") + .put("day", "D") + .put("nights", "D") + .put("night", "D") + .put("hours", "H") + .put("hour", "H") + .put("hrs", "H") + .put("hr", "H") + .put("h", "H") + .put("minutes", "M") + .put("minute", "M") + .put("mins", "M") + .put("min", "M") + .put("seconds", "S") + .put("second", "S") + .put("secs", "S") + .put("sec", "S") + .build(); + + public static final ImmutableMap UnitValueMap = ImmutableMap.builder() + .put("decades", 315360000L) + .put("decade", 315360000L) + .put("years", 31536000L) + .put("year", 31536000L) + .put("months", 2592000L) + .put("month", 2592000L) + .put("fortnights", 1209600L) + .put("fortnight", 1209600L) + .put("weekends", 172800L) + .put("weekend", 172800L) + .put("weeks", 604800L) + .put("week", 604800L) + .put("days", 86400L) + .put("day", 86400L) + .put("nights", 86400L) + .put("night", 86400L) + .put("hours", 3600L) + .put("hour", 3600L) + .put("hrs", 3600L) + .put("hr", 3600L) + .put("h", 3600L) + .put("minutes", 60L) + .put("minute", 60L) + .put("mins", 60L) + .put("min", 60L) + .put("seconds", 1L) + .put("second", 1L) + .put("secs", 1L) + .put("sec", 1L) + .build(); + + public static final ImmutableMap SpecialYearPrefixesMap = ImmutableMap.builder() + .put("fiscal", "FY") + .put("school", "SY") + .put("fy", "FY") + .put("sy", "SY") + .build(); + + public static final ImmutableMap SeasonMap = ImmutableMap.builder() + .put("spring", "SP") + .put("summer", "SU") + .put("fall", "FA") + .put("autumn", "FA") + .put("winter", "WI") + .build(); + + public static final ImmutableMap SeasonValueMap = ImmutableMap.builder() + .put("SP", 3) + .put("SU", 6) + .put("FA", 9) + .put("WI", 12) + .build(); + + public static final ImmutableMap CardinalMap = ImmutableMap.builder() + .put("first", 1) + .put("1st", 1) + .put("second", 2) + .put("2nd", 2) + .put("third", 3) + .put("3rd", 3) + .put("fourth", 4) + .put("4th", 4) + .put("fifth", 5) + .put("5th", 5) + .build(); + + public static final ImmutableMap DayOfWeek = ImmutableMap.builder() + .put("monday", 1) + .put("tuesday", 2) + .put("wednesday", 3) + .put("thursday", 4) + .put("friday", 5) + .put("saturday", 6) + .put("sunday", 0) + .put("mon", 1) + .put("tue", 2) + .put("tues", 2) + .put("wed", 3) + .put("wedn", 3) + .put("weds", 3) + .put("thu", 4) + .put("thur", 4) + .put("thurs", 4) + .put("fri", 5) + .put("sat", 6) + .put("sun", 0) + .build(); + + public static final ImmutableMap MonthOfYear = ImmutableMap.builder() + .put("january", 1) + .put("february", 2) + .put("march", 3) + .put("april", 4) + .put("may", 5) + .put("june", 6) + .put("july", 7) + .put("august", 8) + .put("september", 9) + .put("october", 10) + .put("november", 11) + .put("december", 12) + .put("jan", 1) + .put("feb", 2) + .put("mar", 3) + .put("apr", 4) + .put("jun", 6) + .put("jul", 7) + .put("aug", 8) + .put("sep", 9) + .put("sept", 9) + .put("oct", 10) + .put("nov", 11) + .put("dec", 12) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .build(); + + public static final ImmutableMap Numbers = ImmutableMap.builder() + .put("zero", 0) + .put("one", 1) + .put("a", 1) + .put("an", 1) + .put("two", 2) + .put("three", 3) + .put("four", 4) + .put("five", 5) + .put("six", 6) + .put("seven", 7) + .put("eight", 8) + .put("nine", 9) + .put("ten", 10) + .put("eleven", 11) + .put("twelve", 12) + .put("thirteen", 13) + .put("fourteen", 14) + .put("fifteen", 15) + .put("sixteen", 16) + .put("seventeen", 17) + .put("eighteen", 18) + .put("nineteen", 19) + .put("twenty", 20) + .put("twenty one", 21) + .put("twenty two", 22) + .put("twenty three", 23) + .put("twenty four", 24) + .put("twenty five", 25) + .put("twenty six", 26) + .put("twenty seven", 27) + .put("twenty eight", 28) + .put("twenty nine", 29) + .put("thirty", 30) + .put("thirty one", 31) + .put("thirty two", 32) + .put("thirty three", 33) + .put("thirty four", 34) + .put("thirty five", 35) + .put("thirty six", 36) + .put("thirty seven", 37) + .put("thirty eight", 38) + .put("thirty nine", 39) + .put("forty", 40) + .put("forty one", 41) + .put("forty two", 42) + .put("forty three", 43) + .put("forty four", 44) + .put("forty five", 45) + .put("forty six", 46) + .put("forty seven", 47) + .put("forty eight", 48) + .put("forty nine", 49) + .put("fifty", 50) + .put("fifty one", 51) + .put("fifty two", 52) + .put("fifty three", 53) + .put("fifty four", 54) + .put("fifty five", 55) + .put("fifty six", 56) + .put("fifty seven", 57) + .put("fifty eight", 58) + .put("fifty nine", 59) + .put("sixty", 60) + .put("sixty one", 61) + .put("sixty two", 62) + .put("sixty three", 63) + .put("sixty four", 64) + .put("sixty five", 65) + .put("sixty six", 66) + .put("sixty seven", 67) + .put("sixty eight", 68) + .put("sixty nine", 69) + .put("seventy", 70) + .put("seventy one", 71) + .put("seventy two", 72) + .put("seventy three", 73) + .put("seventy four", 74) + .put("seventy five", 75) + .put("seventy six", 76) + .put("seventy seven", 77) + .put("seventy eight", 78) + .put("seventy nine", 79) + .put("eighty", 80) + .put("eighty one", 81) + .put("eighty two", 82) + .put("eighty three", 83) + .put("eighty four", 84) + .put("eighty five", 85) + .put("eighty six", 86) + .put("eighty seven", 87) + .put("eighty eight", 88) + .put("eighty nine", 89) + .put("ninety", 90) + .put("ninety one", 91) + .put("ninety two", 92) + .put("ninety three", 93) + .put("ninety four", 94) + .put("ninety five", 95) + .put("ninety six", 96) + .put("ninety seven", 97) + .put("ninety eight", 98) + .put("ninety nine", 99) + .put("one hundred", 100) + .build(); + + public static final ImmutableMap DayOfMonth = ImmutableMap.builder() + .put("1st", 1) + .put("1th", 1) + .put("2nd", 2) + .put("2th", 2) + .put("3rd", 3) + .put("3th", 3) + .put("4th", 4) + .put("5th", 5) + .put("6th", 6) + .put("7th", 7) + .put("8th", 8) + .put("9th", 9) + .put("10th", 10) + .put("11th", 11) + .put("11st", 11) + .put("12th", 12) + .put("12nd", 12) + .put("13th", 13) + .put("13rd", 13) + .put("14th", 14) + .put("15th", 15) + .put("16th", 16) + .put("17th", 17) + .put("18th", 18) + .put("19th", 19) + .put("20th", 20) + .put("21st", 21) + .put("21th", 21) + .put("22nd", 22) + .put("22th", 22) + .put("23rd", 23) + .put("23th", 23) + .put("24th", 24) + .put("25th", 25) + .put("26th", 26) + .put("27th", 27) + .put("28th", 28) + .put("29th", 29) + .put("30th", 30) + .put("31st", 31) + .put("01st", 1) + .put("01th", 1) + .put("02nd", 2) + .put("02th", 2) + .put("03rd", 3) + .put("03th", 3) + .put("04th", 4) + .put("05th", 5) + .put("06th", 6) + .put("07th", 7) + .put("08th", 8) + .put("09th", 9) + .build(); + + public static final ImmutableMap DoubleNumbers = ImmutableMap.builder() + .put("half", 0.5D) + .put("quarter", 0.25D) + .build(); + + public static final ImmutableMap HolidayNames = ImmutableMap.builder() + .put("easterday", new String[]{"easterday", "easter", "eastersunday"}) + .put("ashwednesday", new String[]{"ashwednesday"}) + .put("palmsunday", new String[]{"palmsunday"}) + .put("maundythursday", new String[]{"maundythursday"}) + .put("goodfriday", new String[]{"goodfriday"}) + .put("eastersaturday", new String[]{"eastersaturday"}) + .put("eastermonday", new String[]{"eastermonday"}) + .put("ascensionday", new String[]{"ascensionday"}) + .put("whitesunday", new String[]{"whitesunday", "pentecost", "pentecostday"}) + .put("whitemonday", new String[]{"whitemonday"}) + .put("trinitysunday", new String[]{"trinitysunday"}) + .put("corpuschristi", new String[]{"corpuschristi"}) + .put("earthday", new String[]{"earthday"}) + .put("fathers", new String[]{"fatherday", "fathersday"}) + .put("mothers", new String[]{"motherday", "mothersday"}) + .put("thanksgiving", new String[]{"thanksgivingday", "thanksgiving"}) + .put("blackfriday", new String[]{"blackfriday"}) + .put("cybermonday", new String[]{"cybermonday"}) + .put("martinlutherking", new String[]{"mlkday", "martinlutherkingday", "martinlutherkingjrday"}) + .put("washingtonsbirthday", new String[]{"washingtonsbirthday", "washingtonbirthday", "presidentsday"}) + .put("canberra", new String[]{"canberraday"}) + .put("labour", new String[]{"labourday", "laborday"}) + .put("columbus", new String[]{"columbusday"}) + .put("memorial", new String[]{"memorialday"}) + .put("yuandan", new String[]{"yuandan"}) + .put("maosbirthday", new String[]{"maosbirthday"}) + .put("teachersday", new String[]{"teachersday", "teacherday"}) + .put("singleday", new String[]{"singleday"}) + .put("allsaintsday", new String[]{"allsaintsday"}) + .put("youthday", new String[]{"youthday"}) + .put("childrenday", new String[]{"childrenday", "childday"}) + .put("femaleday", new String[]{"femaleday"}) + .put("treeplantingday", new String[]{"treeplantingday"}) + .put("arborday", new String[]{"arborday"}) + .put("girlsday", new String[]{"girlsday"}) + .put("whiteloverday", new String[]{"whiteloverday"}) + .put("loverday", new String[]{"loverday", "loversday"}) + .put("christmas", new String[]{"christmasday", "christmas"}) + .put("xmas", new String[]{"xmasday", "xmas"}) + .put("newyear", new String[]{"newyear"}) + .put("newyearday", new String[]{"newyearday"}) + .put("newyearsday", new String[]{"newyearsday"}) + .put("inaugurationday", new String[]{"inaugurationday"}) + .put("groundhougday", new String[]{"groundhougday"}) + .put("valentinesday", new String[]{"valentinesday"}) + .put("stpatrickday", new String[]{"stpatrickday", "stpatricksday", "stpatrick"}) + .put("aprilfools", new String[]{"aprilfools"}) + .put("stgeorgeday", new String[]{"stgeorgeday"}) + .put("mayday", new String[]{"mayday", "intlworkersday", "internationalworkersday", "workersday"}) + .put("cincodemayoday", new String[]{"cincodemayoday"}) + .put("baptisteday", new String[]{"baptisteday"}) + .put("usindependenceday", new String[]{"usindependenceday"}) + .put("independenceday", new String[]{"independenceday"}) + .put("bastilleday", new String[]{"bastilleday"}) + .put("halloweenday", new String[]{"halloweenday", "halloween"}) + .put("allhallowday", new String[]{"allhallowday"}) + .put("allsoulsday", new String[]{"allsoulsday"}) + .put("guyfawkesday", new String[]{"guyfawkesday"}) + .put("veteransday", new String[]{"veteransday"}) + .put("christmaseve", new String[]{"christmaseve"}) + .put("newyeareve", new String[]{"newyearseve", "newyeareve"}) + .put("juneteenth", new String[]{"juneteenth", "freedomday", "jubileeday"}) + .build(); + + public static final ImmutableMap WrittenDecades = ImmutableMap.builder() + .put("hundreds", 0) + .put("tens", 10) + .put("twenties", 20) + .put("thirties", 30) + .put("forties", 40) + .put("fifties", 50) + .put("sixties", 60) + .put("seventies", 70) + .put("eighties", 80) + .put("nineties", 90) + .build(); + + public static final ImmutableMap SpecialDecadeCases = ImmutableMap.builder() + .put("noughties", 2000) + .put("aughts", 2000) + .put("two thousands", 2000) + .build(); + + public static final String DefaultLanguageFallback = "MDY"; + + public static final List SuperfluousWordList = Arrays.asList("preferably", "how about", "maybe", "perhaps", "say", "like"); + + public static final List DurationDateRestrictions = Arrays.asList("today", "now"); + + public static final ImmutableMap AmbiguityFiltersDict = ImmutableMap.builder() + .put("^(morning|afternoon|evening|night|day)\\b", "\\b(good\\s+(morning|afternoon|evening|night|day))|(nighty\\s+night)\\b") + .put("\\bnow\\b", "\\b(^now,)|\\b((is|are)\\s+now\\s+for|for\\s+now)\\b") + .put("\\bmay\\b", "\\b((((!|\\.|\\?|,|;|)\\s+|^)may i)|(i|you|he|she|we|they)\\s+may|(may\\s+((((also|not|(also not)|well)\\s+)?(be|ask|contain|constitute|e-?mail|take|have|result|involve|get|work|reply|differ))|(or may not))))\\b") + .put("\\b(a|one) second\\b", "\\b(? MorningTermList = Arrays.asList("morning"); + + public static final List AfternoonTermList = Arrays.asList("afternoon"); + + public static final List EveningTermList = Arrays.asList("evening"); + + public static final List MealtimeBreakfastTermList = Arrays.asList("breakfast"); + + public static final List MealtimeBrunchTermList = Arrays.asList("brunch"); + + public static final List MealtimeLunchTermList = Arrays.asList("lunch", "lunchtime"); + + public static final List MealtimeDinnerTermList = Arrays.asList("dinner", "dinnertime", "supper"); + + public static final List DaytimeTermList = Arrays.asList("daytime"); + + public static final List NightTermList = Arrays.asList("night"); + + public static final List SameDayTerms = Arrays.asList("today", "otd"); + + public static final List PlusOneDayTerms = Arrays.asList("tomorrow", "tmr", "day after"); + + public static final List MinusOneDayTerms = Arrays.asList("yesterday", "day before"); + + public static final List PlusTwoDayTerms = Arrays.asList("day after tomorrow", "day after tmr"); + + public static final List MinusTwoDayTerms = Arrays.asList("day before yesterday"); + + public static final List FutureTerms = Arrays.asList("this", "next"); + + public static final List LastCardinalTerms = Arrays.asList("last"); + + public static final List MonthTerms = Arrays.asList("month"); + + public static final List MonthToDateTerms = Arrays.asList("month to date"); + + public static final List WeekendTerms = Arrays.asList("weekend"); + + public static final List WeekTerms = Arrays.asList("week"); + + public static final List YearTerms = Arrays.asList("year"); + + public static final List GenericYearTerms = Arrays.asList("y"); + + public static final List YearToDateTerms = Arrays.asList("year to date"); + + public static final String DoubleMultiplierRegex = "^(bi)(-|\\s)?"; + + public static final String HalfMultiplierRegex = "^(semi)(-|\\s)?"; + + public static final String DayTypeRegex = "((week)?da(il)?ys?)$"; + + public static final String WeekTypeRegex = "(week(s|ly)?)$"; + + public static final String WeekendTypeRegex = "(weekends?)$"; + + public static final String MonthTypeRegex = "(month(s|ly)?)$"; + + public static final String QuarterTypeRegex = "(quarter(s|ly)?)$"; + + public static final String YearTypeRegex = "((years?|annual)(ly)?)$"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/EnglishTimeZone.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/EnglishTimeZone.java new file mode 100644 index 000000000..dd5e8a470 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/EnglishTimeZone.java @@ -0,0 +1,374 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class EnglishTimeZone { + + public static final String DirectUtcRegex = "\\b(utc|gmt)(\\s*[+\\-\\u00B1]?\\s*[\\d]{1,2}h?(\\s*:\\s*[\\d]{1,2})?)?\\b"; + + public static final List AbbreviationsList = Arrays.asList("ABST", "ACDT", "ACST", "ACT", "ADT", "AEDT", "AEST", "AET", "AFT", "AKDT", "AKST", "AMST", "AMT", "AOE", "AoE", "ARBST", "ARST", "ART", "AST", "AWDT", "AWST", "AZOST", "AZOT", "AZST", "AZT", "BIT", "BST", "BTT", "CADT", "CAST", "CBST", "CBT", "CCST", "CDT", "CDTM", "CEST", "CET", "COT", "CST", "CSTM", "CT", "CVT", "EAT", "ECT", "EDT", "EDTM", "EEST", "EET", "EGST", "ESAST", "ESAT", "EST", "ESTM", "ET", "FJST", "FJT", "GET", "GMT", "GNDT", "GNST", "GST", "GTBST", "HADT", "HAST", "HDT", "HKT", "HST", "IRDT", "IRKT", "IRST", "ISDT", "ISST", "IST", "JDT", "JST", "KRAT", "KST", "LINT", "MAGST", "MAGT", "MAT", "MDT", "MDTM", "MEST", "MOST", "MSK", "MSK+1", "MSK+2", "MSK+3", "MSK+4", "MSK+5", "MSK+6", "MSK+7", "MSK+8", "MSK+9", "MSK-1", "MST", "MSTM", "MUT", "MVST", "MYST", "NCAST", "NDT", "NMDT", "NMST", "NPT", "NST", "NZDT", "NZST", "NZT", "PDST", "PDT", "PDTM", "PETT", "PKT", "PSAST", "PSAT", "PST", "PSTM", "PT", "PYST", "PYT", "RST", "SAEST", "SAPST", "SAST", "SAWST", "SBT", "SGT", "SLT", "SMST", "SNST", "SST", "TADT", "TAST", "THA", "TIST", "TOST", "TOT", "TRT", "TST", "ULAT", "UTC", "VET", "VLAT", "WAST", "WAT", "WEST", "WET", "WPST", "YAKT", "YEKT"); + + public static final List FullNameList = Arrays.asList("Acre Time", "Afghanistan Standard Time", "Alaskan Standard Time", "Anywhere on Earth", "Arab Standard Time", "Arabian Standard Time", "Arabic Standard Time", "Argentina Standard Time", "Atlantic Standard Time", "AUS Central Standard Time", "Australian Central Time", "AUS Eastern Standard Time", "Australian Eastern Time", "Australian Eastern Standard Time", "Australian Central Daylight Time", "Australian Eastern Daylight Time", "Azerbaijan Standard Time", "Azores Standard Time", "Bahia Standard Time", "Bangladesh Standard Time", "Belarus Standard Time", "Canada Central Standard Time", "Cape Verde Standard Time", "Caucasus Standard Time", "Cen. Australia Standard Time", "Central America Standard Time", "Central Asia Standard Time", "Central Brazilian Standard Time", "Central Daylight Time", "Europe Central Time", "European Central Time", "Central Europe Standard Time", "Central Europe Std Time", "Central European Std Time", "Central European Standard Time", "Central Pacific Standard Time", "Central Standard Time", "Central Standard Time (Mexico)", "China Standard Time", "Dateline Standard Time", "E. Africa Standard Time", "E. Australia Standard Time", "E. Europe Standard Time", "E. South America Standard Time", "Eastern Time", "Eastern Daylight Time", "Eastern Standard Time", "Eastern Standard Time (Mexico)", "Egypt Standard Time", "Ekaterinburg Standard Time", "Fiji Standard Time", "FLE Standard Time", "Georgian Standard Time", "GMT Standard Time", "Greenland Standard Time", "Greenwich Standard Time", "GTB Standard Time", "Hawaiian Standard Time", "India Standard Time", "Iran Standard Time", "Israel Standard Time", "Jordan Standard Time", "Kaliningrad Standard Time", "Kamchatka Standard Time", "Korea Standard Time", "Libya Standard Time", "Line Islands Standard Time", "Magadan Standard Time", "Mauritius Standard Time", "Mid-Atlantic Standard Time", "Middle East Standard Time", "Montevideo Standard Time", "Morocco Standard Time", "Mountain Standard Time", "Mountain Standard Time (Mexico)", "Myanmar Standard Time", "N. Central Asia Standard Time", "Namibia Standard Time", "Nepal Standard Time", "New Zealand Standard Time", "Newfoundland Standard Time", "North Asia East Standard Time", "North Asia Standard Time", "North Korea Standard Time", "Pacific SA Standard Time", "Pacific Standard Time", "Pacific Daylight Time", "Pacific Time", "Pacific Standard Time", "Pacific Standard Time (Mexico)", "Pakistan Standard Time", "Paraguay Standard Time", "Romance Standard Time", "Russia Time Zone 1", "Russia Time Zone 2", "Russia Time Zone 3", "Russia Time Zone 4", "Russia Time Zone 5", "Russia Time Zone 6", "Russia Time Zone 7", "Russia Time Zone 8", "Russia Time Zone 9", "Russia Time Zone 10", "Russia Time Zone 11", "Russian Standard Time", "SA Eastern Standard Time", "SA Pacific Standard Time", "SA Western Standard Time", "Samoa Standard Time", "SE Asia Standard Time", "Singapore Standard Time", "Singapore Time", "South Africa Standard Time", "Sri Lanka Standard Time", "Syria Standard Time", "Taipei Standard Time", "Tasmania Standard Time", "Tokyo Standard Time", "Tonga Standard Time", "Turkey Standard Time", "Ulaanbaatar Standard Time", "US Eastern Standard Time", "US Mountain Standard Time", "Mountain", "Venezuela Standard Time", "Vladivostok Standard Time", "W. Australia Standard Time", "W. Central Africa Standard Time", "W. Europe Standard Time", "West Asia Standard Time", "West Pacific Standard Time", "Yakutsk Standard Time", "Pacific Daylight Saving Time", "Austrialian Western Daylight Time", "Austrialian West Daylight Time", "Australian Western Daylight Time", "Australian West Daylight Time", "Colombia Time", "Hong Kong Time", "Central Europe Time", "Central European Time", "Central Europe Summer Time", "Central European Summer Time", "Central Europe Standard Time", "Central European Standard Time", "Central Europe Std Time", "Central European Std Time", "West Coast Time", "West Coast", "Central Time", "Central", "Pacific", "Eastern"); + + public static final String BaseTimeZoneSuffixRegex = "((\\s+|-)(friendly|compatible))?(\\s+|-)time(zone)?"; + + public static final String LocationTimeSuffixRegex = "({BaseTimeZoneSuffixRegex})\\b" + .replace("{BaseTimeZoneSuffixRegex}", BaseTimeZoneSuffixRegex); + + public static final String TimeZoneEndRegex = "({BaseTimeZoneSuffixRegex})$" + .replace("{BaseTimeZoneSuffixRegex}", BaseTimeZoneSuffixRegex); + + public static final List AmbiguousTimezoneList = Arrays.asList("bit", "get", "art", "cast", "eat", "lint", "mat", "most", "west", "vet", "wet", "cot", "pt", "et", "eastern", "pacific", "central", "mountain", "west coast"); + + public static final ImmutableMap AbbrToMinMapping = ImmutableMap.builder() + .put("abst", 180) + .put("acdt", 630) + .put("acst", 570) + .put("act", -10000) + .put("adt", -10000) + .put("aedt", 660) + .put("aest", 600) + .put("aet", 600) + .put("aft", 270) + .put("akdt", -480) + .put("akst", -540) + .put("amst", -10000) + .put("amt", -10000) + .put("aoe", -720) + .put("arbst", 180) + .put("arst", 180) + .put("art", -180) + .put("ast", -10000) + .put("awdt", 540) + .put("awst", 480) + .put("azost", 0) + .put("azot", -60) + .put("azst", 300) + .put("azt", 240) + .put("bit", -720) + .put("bst", -10000) + .put("btt", 360) + .put("cadt", -360) + .put("cast", 480) + .put("cbst", -240) + .put("cbt", -240) + .put("ccst", -360) + .put("cdt", -10000) + .put("cdtm", -360) + .put("cest", 120) + .put("cet", 60) + .put("cot", -300) + .put("cst", -10000) + .put("cstm", -360) + .put("ct", -360) + .put("cvt", -60) + .put("eat", 180) + .put("ect", -10000) + .put("edt", -240) + .put("edtm", -300) + .put("eest", 180) + .put("eet", 120) + .put("egst", 0) + .put("esast", -180) + .put("esat", -180) + .put("est", -300) + .put("estm", -300) + .put("et", -300) + .put("fjst", 780) + .put("fjt", 720) + .put("get", 240) + .put("gmt", 0) + .put("gndt", -180) + .put("gnst", -180) + .put("gst", -10000) + .put("gtbst", 120) + .put("hadt", -540) + .put("hast", -600) + .put("hdt", -540) + .put("hkt", 480) + .put("hst", -600) + .put("irdt", 270) + .put("irkt", 480) + .put("irst", 210) + .put("isdt", 120) + .put("isst", 120) + .put("ist", -10000) + .put("jdt", 120) + .put("jst", 540) + .put("krat", 420) + .put("kst", -10000) + .put("lint", 840) + .put("magst", 720) + .put("magt", 660) + .put("mat", -120) + .put("mdt", -360) + .put("mdtm", -420) + .put("mest", 120) + .put("most", 0) + .put("msk+1", 240) + .put("msk+2", 300) + .put("msk+3", 360) + .put("msk+4", 420) + .put("msk+5", 480) + .put("msk+6", 540) + .put("msk+7", 600) + .put("msk+8", 660) + .put("msk+9", 720) + .put("msk-1", 120) + .put("msk", 180) + .put("mst", -420) + .put("mstm", -420) + .put("mut", 240) + .put("mvst", -180) + .put("myst", 390) + .put("ncast", 420) + .put("ndt", -150) + .put("nmdt", 60) + .put("nmst", 60) + .put("npt", 345) + .put("nst", -210) + .put("nzdt", 780) + .put("nzst", 720) + .put("nzt", 720) + .put("pdst", -420) + .put("pdt", -420) + .put("pdtm", -480) + .put("pett", 720) + .put("pkt", 300) + .put("psast", -240) + .put("psat", -240) + .put("pst", -480) + .put("pstm", -480) + .put("pt", -420) + .put("pyst", -10000) + .put("pyt", -10000) + .put("rst", 60) + .put("saest", -180) + .put("sapst", -300) + .put("sast", 120) + .put("sawst", -240) + .put("sbt", 660) + .put("sgt", 480) + .put("slt", 330) + .put("smst", 780) + .put("snst", 480) + .put("sst", -10000) + .put("tadt", 600) + .put("tast", 600) + .put("tha", 420) + .put("tist", 480) + .put("tost", 840) + .put("tot", 780) + .put("trt", 180) + .put("tst", 540) + .put("ulat", 480) + .put("utc", 0) + .put("vet", -240) + .put("vlat", 600) + .put("wast", 120) + .put("wat", -10000) + .put("west", 60) + .put("wet", 0) + .put("wpst", 600) + .put("yakt", 540) + .put("yekt", 300) + .build(); + + public static final ImmutableMap FullToMinMapping = ImmutableMap.builder() + .put("beijing", 480) + .put("shanghai", 480) + .put("shenzhen", 480) + .put("suzhou", 480) + .put("tianjian", 480) + .put("chengdu", 480) + .put("guangzhou", 480) + .put("wuxi", 480) + .put("xiamen", 480) + .put("chongqing", 480) + .put("shenyang", 480) + .put("china", 480) + .put("redmond", -480) + .put("seattle", -480) + .put("bellevue", -480) + .put("pacific daylight", -420) + .put("pacific", -480) + .put("afghanistan standard", 270) + .put("alaskan standard", -540) + .put("anywhere on earth", -720) + .put("arab standard", 180) + .put("arabian standard", 180) + .put("arabic standard", 180) + .put("argentina standard", -180) + .put("atlantic standard", -240) + .put("aus central standard", 570) + .put("aus eastern standard", 600) + .put("australian eastern", 600) + .put("australian eastern standard", 600) + .put("australian central daylight", 630) + .put("australian eastern daylight", 660) + .put("azerbaijan standard", 240) + .put("azores standard", -60) + .put("bahia standard", -180) + .put("bangladesh standard", 360) + .put("belarus standard", 180) + .put("canada central standard", -360) + .put("cape verde standard", -60) + .put("caucasus standard", 240) + .put("cen. australia standard", 570) + .put("central australia standard", 570) + .put("central america standard", -360) + .put("central asia standard", 360) + .put("central brazilian standard", -240) + .put("central daylight", -10000) + .put("central europe", 60) + .put("central european", 60) + .put("central europe std", 60) + .put("central european std", 60) + .put("central europe standard", 60) + .put("central european standard", 60) + .put("central europe summer", 120) + .put("central european summer", 120) + .put("central pacific standard", 660) + .put("central standard time (mexico)", -360) + .put("central standard", -360) + .put("china standard", 480) + .put("dateline standard", -720) + .put("e. africa standard", 180) + .put("e. australia standard", 600) + .put("e. europe standard", 120) + .put("e. south america standard", -180) + .put("europe central", 60) + .put("european central", 60) + .put("central", -300) + .put("eastern", -240) + .put("eastern daylight", -10000) + .put("eastern standard time (mexico)", -300) + .put("eastern standard", -300) + .put("egypt standard", 120) + .put("ekaterinburg standard", 300) + .put("fiji standard", 720) + .put("fle standard", 120) + .put("georgian standard", 240) + .put("gmt standard", 0) + .put("greenland standard", -180) + .put("greenwich standard", 0) + .put("gtb standard", 120) + .put("hawaiian standard", -600) + .put("india standard", 330) + .put("iran standard", 210) + .put("israel standard", 120) + .put("jordan standard", 120) + .put("kaliningrad standard", 120) + .put("kamchatka standard", 720) + .put("korea standard", 540) + .put("libya standard", 120) + .put("line islands standard", 840) + .put("magadan standard", 660) + .put("mauritius standard", 240) + .put("mid-atlantic standard", -120) + .put("middle east standard", 120) + .put("montevideo standard", -180) + .put("morocco standard", 0) + .put("mountain", -360) + .put("mountain standard", -420) + .put("mountain standard time (mexico)", -420) + .put("myanmar standard", 390) + .put("n. central asia standard", 420) + .put("namibia standard", 60) + .put("nepal standard", 345) + .put("new zealand standard", 720) + .put("newfoundland standard", -210) + .put("north asia east standard", 480) + .put("north asia standard", 420) + .put("north korea standard", 510) + .put("west coast", -420) + .put("pacific sa standard", -240) + .put("pacific standard", -480) + .put("pacific standard time (mexico)", -480) + .put("pakistan standard", 300) + .put("paraguay standard", -240) + .put("romance standard", 60) + .put("russia time zone 1", 120) + .put("russia time zone 2", 180) + .put("russia time zone 3", 240) + .put("russia time zone 4", 300) + .put("russia time zone 5", 360) + .put("russia time zone 6", 420) + .put("russia time zone 7", 480) + .put("russia time zone 8", 540) + .put("russia time zone 9", 600) + .put("russia time zone 10", 660) + .put("russia time zone 11", 720) + .put("russian standard", 180) + .put("sa eastern standard", -180) + .put("sa pacific standard", -300) + .put("sa western standard", -240) + .put("samoa standard", -660) + .put("se asia standard", 420) + .put("singapore standard", 480) + .put("singapore", 480) + .put("south africa standard", 120) + .put("sri lanka standard", 330) + .put("syria standard", 120) + .put("taipei standard", 480) + .put("tasmania standard", 600) + .put("tokyo standard", 540) + .put("tonga standard", 780) + .put("turkey standard", 180) + .put("ulaanbaatar standard", 480) + .put("us eastern standard", -300) + .put("us mountain standard", -420) + .put("venezuela standard", -240) + .put("vladivostok standard", 600) + .put("w. australia standard", 480) + .put("w. central africa standard", 60) + .put("w. europe standard", 0) + .put("western european", 0) + .put("west europe standard", 0) + .put("west europe std", 0) + .put("western europe standard", 0) + .put("western europe summer", 60) + .put("w. europe summer", 60) + .put("western european summer", 60) + .put("west europe summer", 60) + .put("west asia standard", 300) + .put("west pacific standard", 600) + .put("yakutsk standard", 540) + .put("pacific daylight saving", -420) + .put("australian western daylight", 540) + .put("australian west daylight", 540) + .put("austrialian western daylight", 540) + .put("austrialian west daylight", 540) + .put("colombia", -300) + .put("hong kong", 480) + .put("madrid", 60) + .put("bilbao", 60) + .put("seville", 60) + .put("valencia", 60) + .put("malaga", 60) + .put("las Palmas", 60) + .put("zaragoza", 60) + .put("alicante", 60) + .put("alche", 60) + .put("oviedo", 60) + .put("gijón", 60) + .put("avilés", 60) + .build(); + + public static final List MajorLocations = Arrays.asList("Dominican Republic", "Dominica", "Guinea Bissau", "Guinea-Bissau", "Guinea", "Equatorial Guinea", "Papua New Guinea", "New York City", "New York", "York", "Mexico City", "New Mexico", "Mexico", "Aberdeen", "Adelaide", "Anaheim", "Atlanta", "Auckland", "Austin", "Bangkok", "Baltimore", "Baton Rouge", "Beijing", "Belfast", "Birmingham", "Bolton", "Boston", "Bournemouth", "Bradford", "Brisbane", "Bristol", "Calgary", "Canberra", "Cardiff", "Charlotte", "Chicago", "Christchurch", "Colchester", "Colorado Springs", "Coventry", "Dallas", "Denver", "Derby", "Detroit", "Dubai", "Dublin", "Dudley", "Dunedin", "Edinburgh", "Edmonton", "El Paso", "Glasgow", "Gold Coast", "Hamilton", "Hialeah", "Houston", "Ipswich", "Jacksonville", "Jersey City", "Kansas City", "Kingston-upon-Hull", "Leeds", "Leicester", "Lexington", "Lincoln", "Liverpool", "London", "Long Beach", "Los Angeles", "Louisville", "Lubbock", "Luton", "Madison", "Manchester", "Mansfield", "Melbourne", "Memphis", "Mesa", "Miami", "Middlesbrough", "Milan", "Milton Keynes", "Minneapolis", "Montréal", "Montreal", "Nashville", "New Orleans", "Newark", "Newcastle-upon-Tyne", "Newcastle", "Northampton", "Norwich", "Nottingham", "Oklahoma City", "Oldham", "Omaha", "Orlando", "Ottawa", "Perth", "Peterborough", "Philadelphia", "Phoenix", "Plymouth", "Portland", "Portsmouth", "Preston", "Québec City", "Quebec City", "Québec", "Quebec", "Raleigh", "Reading", "Redmond", "Richmond", "Rome", "San Antonio", "San Diego", "San Francisco", "San José", "Santa Ana", "Seattle", "Sheffield", "Southampton", "Southend-on-Sea", "Spokane", "St Louis", "St Paul", "St Petersburg", "St. Louis", "St. Paul", "St. Petersburg", "Stockton-on-Tees", "Stockton", "Stoke-on-Trent", "Sunderland", "Swansea", "Swindon", "Sydney", "Tampa", "Tauranga", "Telford", "Toronto", "Vancouver", "Virginia Beach", "Walsall", "Warrington", "Washington", "Wellington", "Wolverhampton", "Abilene", "Akron", "Albuquerque", "Alexandria", "Allentown", "Amarillo", "Anchorage", "Ann Arbor", "Antioch", "Arlington", "Arvada", "Athens", "Augusta", "Aurora", "Bakersfield", "Beaumont", "Bellevue", "Berkeley", "Billings", "Boise", "Boulder", "Bridgeport", "Broken Arrow", "Brownsville", "Buffalo", "Burbank", "Cambridge", "Cape Coral", "Carlsbad", "Carrollton", "Cary", "Cedar Rapids", "Centennial", "Chandler", "Charleston", "Chattanooga", "Chengdu", "Chesapeake", "Chongqing", "Chula Vista", "Cincinnati", "Clarksville", "Clearwater", "Cleveland", "Clovis", "College Station", "Columbia", "Columbus", "Concord", "Coral Springs", "Corona", "Costa Mesa", "Daly City", "Davenport", "Dayton", "Denton", "Des Moines", "Downey", "Durham", "Edison", "El Cajon", "El Monte", "Elgin", "Elizabeth", "Elk Grove", "Erie", "Escondido", "Eugene", "Evansville", "Everett", "Fairfield", "Fargo", "Farmington Hills", "Fayetteville", "Fontana", "Fort Collins", "Fort Lauderdale", "Fort Wayne", "Fort Worth", "Fremont", "Fresno", "Frisco", "Fullerton", "Gainesville", "Garden Grove", "Garland", "Gilbert", "Glendale", "Grand Prairie", "Grand Rapids", "Green Bay", "Greensboro", "Gresham", "Guangzhou", "Hampton", "Hartford", "Hayward", "Henderson", "High Point", "Hollywood", "Honolulu", "Huntington Beach", "Huntsville", "Independence", "Indianapolis", "Inglewood", "Irvine", "Irving", "Jackson", "Joliet", "Kent", "Killeen", "Knoxville", "Lafayette", "Lakeland", "Lakewood", "Lancaster", "Lansing", "Laredo", "Las Cruces", "Las Vegas", "Lewisville", "Little Rock", "Lowell", "Macon", "McAllen", "McKinney", "Mesquite", "Miami Gardens", "Midland", "Milwaukee", "Miramar", "Mobile", "Modesto", "Montgomery", "Moreno Valley", "Murfreesboro", "Murrieta", "Naperville", "New Haven", "Newport News", "Norfolk", "Norman", "North Charleston", "North Las Vegas", "Norwalk", "Oakland", "Oceanside", "Odessa", "Olathe", "Ontario", "Orange", "Overland Park", "Oxnard", "Palm Bay", "Palmdale", "Pasadena", "Paterson", "Pearland", "Pembroke Pines", "Peoria", "Pittsburgh", "Plano", "Pomona", "Pompano Beach", "Providence", "Provo", "Pueblo", "Rancho Cucamonga", "Reno", "Rialto", "Richardson", "Riverside", "Rochester", "Rockford", "Roseville", "Round Rock", "Sacramento", "Saint Paul", "Salem", "Salinas", "Salt Lake City", "San Bernardino", "San Jose", "San Mateo", "Sandy Springs", "Santa Clara", "Santa Clarita", "Santa Maria", "Santa Rosa", "Savannah", "Scottsdale", "Shanghai", "Shenyang", "Shenzhen", "Shreveport", "Simi Valley", "Sioux Falls", "South Bend", "Springfield", "Stamford", "Sterling Heights", "Sunnyvale", "Surprise", "Suzhou", "Syracuse", "Tacoma", "Tallahassee", "Temecula", "Tempe", "Thornton", "Thousand Oaks", "Tianjing", "Toledo", "Topeka", "Torrance", "Tucson", "Tulsa", "Tyler", "Vallejo", "Ventura", "Victorville", "Visalia", "Waco", "Warren", "Waterbury", "West Covina", "West Jordan", "West Palm Beach", "West Valley City", "Westminster", "Wichita", "Wichita Falls", "Wilmington", "Winston-Salem", "Worcester", "Wuxi", "Xiamen", "Yonkers", "Bentonville", "Afghanistan", "AK", "AL", "Alabama", "Åland", "Åland Islands", "Alaska", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "AR", "Argentina", "Arizona", "Arkansas", "Armenia", "Aruba", "Australia", "Austria", "AZ", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bonaire", "Bosnia", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", "CA", "Cabo Verde", "California", "Cambodia", "Cameroon", "Canada", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "CO", "Cocos Islands", "Colombia", "Colorado", "Comoros", "Congo", "Congo (DRC)", "Connecticut", "Cook Islands", "Costa Rica", "Côte d’Ivoire", "Croatia", "CT", "Cuba", "Curaçao", "Cyprus", "Czechia", "DE", "Delaware", "Denmark", "Djibouti", "Ecuador", "Egypt", "El Salvador", "Eritrea", "Estonia", "eSwatini", "Ethiopia", "Falkland Islands", "Falklands", "Faroe Islands", "Fiji", "Finland", "FL", "Florida", "France", "French Guiana", "French Polynesia", "French Southern Territories", "FYROM", "GA", "Gabon", "Gambia", "Georgia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guyana", "Haiti", "Hawaii", "Herzegovina", "HI", "Honduras", "Hong Kong", "Hungary", "IA", "Iceland", "ID", "Idaho", "IL", "Illinois", "IN", "India", "Indiana", "Indonesia", "Iowa", "Iran", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Ivory Coast", "Jamaica", "Jan Mayen", "Japan", "Jersey", "Jordan", "Kansas", "Kazakhstan", "Keeling Islands", "Kentucky", "Kenya", "Kiribati", "Korea", "Kosovo", "KS", "Kuwait", "KY", "Kyrgyzstan", "LA", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Louisiana", "Luxembourg", "MA", "Macao", "Macedonia", "Madagascar", "Maine", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Maryland", "Massachusetts", "Mauritania", "Mauritius", "Mayotte", "MD", "ME", "MI", "Michigan", "Micronesia", "Minnesota", "Mississippi", "Missouri", "MN", "MO", "Moldova", "Monaco", "Mongolia", "Montana", "Montenegro", "Montserrat", "Morocco", "Mozambique", "MS", "MT", "Myanmar", "Namibia", "Nauru", "NC", "ND", "NE", "Nebraska", "Nepal", "Netherlands", "Nevada", "New Caledonia", "New Hampshire", "New Jersey", "New Zealand", "NH", "Nicaragua", "Niger", "Nigeria", "Niue", "NJ", "NM", "Norfolk Island", "North Carolina", "North Dakota", "North Korea", "Northern Mariana Islands", "Norway", "NV", "NY", "OH", "Ohio", "OK", "Oklahoma", "Oman", "OR", "Oregon", "PA", "Pakistan", "Palau", "Palestinian Authority", "Panama", "Paraguay", "Pennsylvania", "Peru", "Philippines", "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar", "Réunion", "Rhode Island", "RI", "Romania", "Russia", "Rwanda", "Saba", "Saint Barthélemy", "Saint Kitts and Nevis", "Saint Lucia", "Saint Martin", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "São Tomé and Príncipe", "Saudi Arabia", "SC", "SD", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Sint Eustatius", "Sint Maarten", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Carolina", "South Dakota", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard", "Swaziland", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Tennessee", "Texas", "Thailand", "Timor-Leste", "TN", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "TX", "U.S. Outlying Islands", "US Outlying Islands", "U.S. Virgin Islands", "US Virgin Islands", "Uganda", "UK", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "US", "USA", "UT", "Utah", "Uzbekistan", "VA", "Vanuatu", "Vatican City", "Venezuela", "Vermont", "Vietnam", "Virginia", "VT", "WA", "Wallis and Futuna", "West Virginia", "WI", "Wisconsin", "WV", "WY", "Wyoming", "Yemen", "Zambia", "Zimbabwe", "Paris", "Tokyo", "Shanghai", "Sao Paulo", "Rio de Janeiro", "Rio", "Brasília", "Brasilia", "Recife", "Milan", "Mumbai", "Moscow", "Frankfurt", "Munich", "Berlim", "Madrid", "Lisbon", "Warsaw", "Johannesburg", "Seoul", "Istanbul", "Kuala Kumpur", "Jakarta", "Amsterdam", "Brussels", "Valencia", "Seville", "Bilbao", "Malaga", "Las Palmas", "Zaragoza", "Alicante", "Elche", "Oviedo", "Gijón", "Avilés", "West Coast", "Central", "Pacific", "Eastern", "Mountain"); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/FrenchDateTime.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/FrenchDateTime.java new file mode 100644 index 000000000..0cee2693f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/FrenchDateTime.java @@ -0,0 +1,1243 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class FrenchDateTime { + + public static final String LangMarker = "Fre"; + + public static final Boolean CheckBothBeforeAfter = false; + + public static final String TillRegex = "(?au|et|(jusqu')?[aà]|avant|--|-|—|——)"; + + public static final String RangeConnectorRegex = "(?de la|au|[aà]|et(\\s*la)?|--|-|—|——)"; + + public static final String RelativeRegex = "(?prochaine?|de|du|ce(tte)?|l[ae]|derni[eè]re|hier|pr[eé]c[eé]dente|au\\s+cours+(de|du\\s*))"; + + public static final String StrictRelativeRegex = "(?prochaine?|derni[eè]re|hier|pr[eé]c[eé]dente|au\\s+cours+(de|du\\s*))"; + + public static final String NextSuffixRegex = "(?prochaines?|prochain|suivante)\\b"; + + public static final String PastSuffixRegex = "(?derni[eè]re?|pr[eé]c[eé]dente)\\b"; + + public static final String ThisPrefixRegex = "(?ce(tte)?|au\\s+cours+(du|de))\\b"; + + public static final String RangePrefixRegex = "(du|depuis|des?|entre)"; + + public static final String DayRegex = "(?(?:3[0-1]|[1-2]\\d|0?[1-9])(e(r)?)?)(?=\\b|t)"; + + public static final String MonthNumRegex = "(?1[0-2]|(0)?[1-9])\\b"; + + public static final String SpecialDescRegex = "(p\\b)"; + + public static final String AmDescRegex = "(h\\b|{BaseDateTime.BaseAmDescRegex})" + .replace("{BaseDateTime.BaseAmDescRegex}", BaseDateTime.BaseAmDescRegex); + + public static final String PmDescRegex = "(h\\b|{BaseDateTime.BasePmDescRegex})" + .replace("{BaseDateTime.BasePmDescRegex}", BaseDateTime.BasePmDescRegex); + + public static final String AmPmDescRegex = "(h\\b|{BaseDateTime.BaseAmPmDescRegex})" + .replace("{BaseDateTime.BaseAmPmDescRegex}", BaseDateTime.BaseAmPmDescRegex); + + public static final String DescRegex = "(?{AmPmDescRegex}|{AmDescRegex}|{PmDescRegex}|{SpecialDescRegex})" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex) + .replace("{AmPmDescRegex}", AmPmDescRegex) + .replace("{SpecialDescRegex}", SpecialDescRegex); + + public static final String TwoDigitYearRegex = "\\b(?([0-9]\\d))(?!(\\s*((\\:\\d)|{AmDescRegex}|{PmDescRegex}|\\.\\d)))\\b" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex); + + public static final String FullTextYearRegex = "^[\\*]"; + + public static final String YearRegex = "({BaseDateTime.FourDigitYearRegex}|{FullTextYearRegex})" + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String WeekDayRegex = "(?dimanche|lundi|mardi|mercredi|jeudi|vendredi|samedi|lun(\\.)?|mar(\\.)?|mer(\\.)?|jeu(\\.)?|ven(\\.)?|sam(\\.)?|dim(\\.)?)"; + + public static final String RelativeMonthRegex = "(?({ThisPrefixRegex}\\s+mois)|(mois\\s+{PastSuffixRegex})|(mois\\s+{NextSuffixRegex}))\\b" + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String WrittenMonthRegex = "(?avril|avr(\\.)?|ao[uû]t|d[eé]cembre|d[eé]c(\\.)?|f[eé]vrier|f[eé]vr?(\\.)?|janvier|janv?(\\.)?|juillet|jui?[ln](\\.)?|mars?(\\.)?|mai|novembre|nov(\\.)?|octobre|oct(\\.)?|septembre|sept?(\\.)?)"; + + public static final String MonthSuffixRegex = "(?(en\\s*|le\\s*|de\\s*|dans\\s*)?({RelativeMonthRegex}|{WrittenMonthRegex}))" + .replace("{RelativeMonthRegex}", RelativeMonthRegex) + .replace("{WrittenMonthRegex}", WrittenMonthRegex); + + public static final String DateUnitRegex = "(?(l')?ann[eé]es?|an|mois|semaines?|journ[eé]es?|jours?)\\b"; + + public static final String SimpleCasesRegex = "\\b((d[ue])|entre\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthFrontSimpleCasesRegex = "\\b((d[ue]|entre)\\s+)?{MonthSuffixRegex}\\s+((d[ue]|entre)\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthFrontBetweenRegex = "\\b{MonthSuffixRegex}\\s+(entre|d[ue]\\s+)({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{YearRegex}", YearRegex); + + public static final String BetweenRegex = "\\b(entre\\s+)({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String YearWordRegex = "\\b(?l'ann[ée]e)\\b"; + + public static final String MonthWithYear = "\\b({WrittenMonthRegex}(\\s*),?(\\s+de)?(\\s*)({YearRegex}|{TwoDigitYearRegex}|(?cette)\\s*{YearWordRegex})|{YearWordRegex}\\s*({PastSuffixRegex}|{NextSuffixRegex}))" + .replace("{WrittenMonthRegex}", WrittenMonthRegex) + .replace("{YearRegex}", YearRegex) + .replace("{TwoDigitYearRegex}", TwoDigitYearRegex) + .replace("{YearWordRegex}", YearWordRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String OneWordPeriodRegex = "\\b(({RelativeRegex}\\s+)?{WrittenMonthRegex}|(la\\s+)?(weekend|(fin de )?semaine|week-end|mois|ans?|l'année)\\s+{StrictRelativeRegex}|{RelativeRegex}\\s+(weekend|(fin de )?semaine|week-end|mois|ans?|l'année)|weekend|week-end|(mois|l'année))\\b" + .replace("{WrittenMonthRegex}", WrittenMonthRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{StrictRelativeRegex}", StrictRelativeRegex); + + public static final String MonthNumWithYear = "({YearRegex}(\\s*)[/\\-\\.](\\s*){MonthNumRegex})|({MonthNumRegex}(\\s*)[/\\-](\\s*){YearRegex})" + .replace("{YearRegex}", YearRegex) + .replace("{MonthNumRegex}", MonthNumRegex); + + public static final String WeekOfMonthRegex = "(?(le\\s+)?(?premier|1er|duexi[èe]me|2|troisi[èe]me|3|quatri[èe]me|4|cinqi[èe]me|5)\\s+semaine(\\s+de)?\\s+{MonthSuffixRegex})" + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String WeekOfYearRegex = "(?(le\\s+)?(?premier|1er|duexi[èe]me|2|troisi[èe]me|3|quatri[èe]me|4|cinqi[èe]me|5)\\s+semaine(\\s+de)?\\s+({YearRegex}|{RelativeRegex}\\s+ann[ée]e))" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String FollowedDateUnit = "^\\s*{DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String NumberCombinedWithDateUnit = "\\b(?\\d+(\\.\\d*)?){DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String QuarterRegex = "(le\\s+)?(?premier|1er|duexi[èe]me|2|troisi[èe]me|3|quatri[èe]me|4)\\s+quart(\\s+de|\\s*,\\s*)?\\s+({YearRegex}|{RelativeRegex}\\s+l'ann[eé]e)" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String QuarterRegexYearFront = "({YearRegex}|l'année\\s+({PastSuffixRegex}|{NextSuffixRegex})|{RelativeRegex}\\s+ann[eé]e)\\s+(le\\s+)?(?premier|1er|duexi[èe]me|2|troisi[èe]me|3|quatri[èe]me|4)\\s+quarts" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String AllHalfYearRegex = "^[.]"; + + public static final String PrefixDayRegex = "\\b((?t[ôo]t\\sdans)|(?au\\smilieu\\sde)|(?tard\\sdans))(\\s+la\\s+journ[ée]e)?$"; + + public static final String CenturySuffixRegex = "^[.]"; + + public static final String SeasonRegex = "\\b((printemps|été|automne|hiver)+\\s*({NextSuffixRegex}|{PastSuffixRegex}))|(?({RelativeRegex}\\s+)?(?printemps|[ée]t[ée]|automne|hiver)((\\s+de|\\s*,\\s*)?\\s+({YearRegex}|{RelativeRegex}\\s+l'ann[eé]e))?)\\b" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex); + + public static final String WhichWeekRegex = "\\b(semaine)(\\s*)(?5[0-3]|[1-4]\\d|0?[1-9])\\b"; + + public static final String WeekOfRegex = "(semaine)(\\s*)(de)"; + + public static final String MonthOfRegex = "(mois)(\\s*)(de)"; + + public static final String MonthRegex = "(?avril|avr(\\.)?|ao[uû]t|d[eé]cembre|d[eé]c(\\.)?|f[eé]vrier|f[eé]vr?(\\.)?|janvier|janv?(\\.)?|juillet|jui?[ln](\\.)?|mars?(\\.)?|mai|novembre|nov(\\.)?|octobre|oct(\\.)?|septembre|sept?(\\.)?)"; + + public static final String OnRegex = "(?<=\\b(en|sur\\s*l[ea]|sur)\\s+)({DayRegex}s?)\\b" + .replace("{DayRegex}", DayRegex); + + public static final String RelaxedOnRegex = "(?<=\\b(en|le|dans|sur\\s*l[ea]|du|sur)\\s+)((?10e|11e|12e|13e|14e|15e|16e|17e|18e|19e|1er|20e|21e|22e|23e|24e|25e|26e|27e|28e|29e|2e|30e|31e|3e|4e|5e|6e|7e|8e|9e)s?)\\b"; + + public static final String ThisRegex = "\\b((cette(\\s*semaine)?\\s+){WeekDayRegex})|({WeekDayRegex}(\\s+cette\\s*semaine))\\b" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String LastDateRegex = "\\b(({WeekDayRegex}(\\s*(de)?\\s*la\\s*semaine\\s+{PastSuffixRegex}))|({WeekDayRegex}(\\s+{PastSuffixRegex})))\\b" + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String NextDateRegex = "\\b(({WeekDayRegex}(\\s+{NextSuffixRegex}))|({WeekDayRegex}(\\s*(de)?\\s*la\\s*semaine\\s+{NextSuffixRegex})))\\b" + .replace("{NextSuffixRegex}", NextSuffixRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String SpecialDayRegex = "\\b(avant[\\s|-]hier|apr[eè]s(-demain|\\s*demain)|(le\\s)?jour suivant|(le\\s+)?dernier jour|hier|lendemain|demain|(de\\s)?la journ[ée]e|aujourd'hui)\\b"; + + public static final String SpecialDayWithNumRegex = "^[.]"; + + public static final String StrictWeekDay = "\\b(?dim(anche)?|lun(di)?|mar(di)?|mer(credi)?|jeu(di)?|ven(dredi)?|sam(edi)?)s?\\b"; + + public static final String SetWeekDayRegex = "\\b(?le\\s+)?(?matin([ée]e)?|apr[eè]s-midi|soir([ée]e)?|dimanche|lundi|mardi|mercredi|jeudi|vendredi|samedi)s\\b"; + + public static final String WeekDayOfMonthRegex = "(?(le\\s+)?(?premier|1er|duexi[èe]me|2|troisi[èe]me|3|quatri[èe]me|4|cinqi[èe]me|5)\\s+{WeekDayRegex}\\s+{MonthSuffixRegex})" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String RelativeWeekDayRegex = "^[.]"; + + public static final String AmbiguousRangeModifierPrefix = "^[.]"; + + public static final String NumberEndingPattern = "^[.]"; + + public static final String SpecialDate = "(?<=\\b(au|le)\\s+){DayRegex}(?!:)\\b" + .replace("{DayRegex}", DayRegex); + + public static final String DateYearRegex = "(?{YearRegex}|{TwoDigitYearRegex})" + .replace("{YearRegex}", YearRegex) + .replace("{TwoDigitYearRegex}", TwoDigitYearRegex); + + public static final String DateExtractor1 = "\\b({WeekDayRegex}(\\s+|\\s*,\\s*))?{MonthRegex}\\s*[/\\\\\\.\\-]?\\s*{DayRegex}(\\s*[/\\\\\\.\\-]?\\s*{BaseDateTime.FourDigitYearRegex})?\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex); + + public static final String DateExtractor2 = "\\b({WeekDayRegex}(\\s+|\\s*,\\s*))?{DayRegex}(\\s+|\\s*,\\s*|\\s+){MonthRegex}\\s*[\\.\\-]?\\s*{DateYearRegex}\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DateExtractor3 = "\\b({WeekDayRegex}(\\s+|\\s*,\\s*))?((?(l')?ann[eé]e(s)?|mois|semaines?)\\b"; + + public static final String HourNumRegex = "\\b(?zero|[aá]\\s+une?|deux|trois|quatre|cinq|six|sept|huit|neuf|onze|douze|treize|quatorze|quinze|dix-six|dix-sept|dix-huit|dix-neuf|vingt|vingt-et-un|vingt-deux|vingt-trois|dix)\\b"; + + public static final String MinuteNumRegex = "(?un|deux|trois|quatre|cinq|six|sept|huit|neuf|onze|douze|treize|quatorze|quinze|seize|dix-sept|dix-huit|dix-neuf|vingt|trente|quarante|cinquante|dix)"; + + public static final String DeltaMinuteNumRegex = "(?un|deux|trois|quatre|cinq|six|sept|huit|neuf|onze|douze|treize|quatorze|quinze|seize|dix-sept|dix-huit|dix-neuf|vingt|trente|quarante|cinquante|dix)"; + + public static final String OclockRegex = "(?heures?|h)"; + + public static final String PmRegex = "(?(dans l'\\s*)?apr[eè]s(\\s*|-)midi|(du|ce|de|le)\\s*(soir([ée]e)?)|(dans l[ea]\\s+)?(nuit|soir[eé]e))"; + + public static final String AmRegex = "(?(du|de|ce|(du|de|dans)\\s*l[ea]|le)?\\s*matin[ée]e|(du|de|ce|dans l[ea]|le)?\\s*matin)"; + + public static final String LessThanOneHour = "(?(une\\s+)?quart|trois quart(s)?|demie( heure)?|{BaseDateTime.DeltaMinuteRegex}(\\s+(minutes?|mins?))|{DeltaMinuteNumRegex}(\\s+(minutes?|mins?)))" + .replace("{BaseDateTime.DeltaMinuteRegex}", BaseDateTime.DeltaMinuteRegex) + .replace("{DeltaMinuteNumRegex}", DeltaMinuteNumRegex); + + public static final String WrittenTimeRegex = "(?{HourNumRegex}\\s+({MinuteNumRegex}|(?vingt|trente|quarante|cinquante)\\s+{MinuteNumRegex}))" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex); + + public static final String TimePrefix = "(?(heures\\s*et\\s+{LessThanOneHour}|et {LessThanOneHour}|{LessThanOneHour} [àa]))" + .replace("{LessThanOneHour}", LessThanOneHour); + + public static final String TimeSuffix = "(?{AmRegex}|{PmRegex}|{OclockRegex})" + .replace("{AmRegex}", AmRegex) + .replace("{PmRegex}", PmRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String BasicTime = "(?{WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}(:|\\s*h\\s*){BaseDateTime.MinuteRegex}(:{BaseDateTime.SecondRegex})?|{BaseDateTime.HourRegex})" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex); + + public static final String MidnightRegex = "(?minuit)"; + + public static final String CommonDatePrefixRegex = "^[\\.]"; + + public static final String MorningRegex = "(?matin([ée]e)?)"; + + public static final String AfternoonRegex = "(?(d'|l')?apr[eè]s(-|\\s*)midi)"; + + public static final String MidmorningRegex = "(?milieu\\s*d[ue]\\s*{MorningRegex})" + .replace("{MorningRegex}", MorningRegex); + + public static final String MiddayRegex = "(?milieu(\\s*|-)d[eu]\\s*(jour|midi)|apr[eè]s(-|\\s*)midi)"; + + public static final String MidafternoonRegex = "(?milieu\\s*d'+{AfternoonRegex})" + .replace("{AfternoonRegex}", AfternoonRegex); + + public static final String MidTimeRegex = "(?({MidnightRegex}|{MidmorningRegex}|{MidafternoonRegex}|{MiddayRegex}))" + .replace("{MidnightRegex}", MidnightRegex) + .replace("{MidmorningRegex}", MidmorningRegex) + .replace("{MidafternoonRegex}", MidafternoonRegex) + .replace("{MiddayRegex}", MiddayRegex); + + public static final String AtRegex = "\\b(((?<=\\b[àa]\\s+)({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}|{MidTimeRegex}))|{MidTimeRegex})\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{MidTimeRegex}", MidTimeRegex); + + public static final String IshRegex = "\\b(peu\\s*pr[èe]s\\s*{BaseDateTime.HourRegex}|peu\\s*pr[èe]s\\s*{WrittenTimeRegex}|peu\\s*pr[èe]s\\s*[àa]\\s*{BaseDateTime.HourRegex}|peu pr[èe]s midi)\\b" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{WrittenTimeRegex}", WrittenTimeRegex); + + public static final String TimeUnitRegex = "(?heures?|hrs?|h|minutes?|mins?|secondes?|secs?)\\b"; + + public static final String RestrictedTimeUnitRegex = "(?huere|minute)\\b"; + + public static final String ConnectNumRegex = "{BaseDateTime.HourRegex}(?[0-5][0-9])\\s*{DescRegex}" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String FivesRegex = "(?(quinze|vingt(\\s*|-*(cinq))?|trente(\\s*|-*(cinq))?|quarante(\\s*|-*(cinq))??|cinquante(\\s*|-*(cinq))?|dix|cinq))\\b"; + + public static final String PeriodHourNumRegex = "(?vingt-et-un|vingt-deux|vingt-trois|vingt-quatre|zero|une|deux|trois|quatre|cinq|six|sept|huit|neuf|dix|onze|douze|treize|quatorze|quinze|seize|dix-sept|dix-huit|dix-neuf|vingt)"; + + public static final String TimeRegex1 = "\\b({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})\\s*{DescRegex}(\\s+{TimePrefix})?\\b" + .replace("{TimePrefix}", TimePrefix) + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex2 = "(\\b{TimePrefix}\\s+)?(t)?{BaseDateTime.HourRegex}(\\s*)?:(\\s*)?{BaseDateTime.MinuteRegex}((\\s*)?:(\\s*)?{BaseDateTime.SecondRegex})?((\\s*{DescRegex})|\\b)" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex3 = "\\b{BaseDateTime.HourRegex}\\.{BaseDateTime.MinuteRegex}(\\s*{DescRegex})(\\s+{TimePrefix})?" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex4 = "\\b{BasicTime}(\\s*{DescRegex})?(\\s+{TimePrefix})?\\s+{TimeSuffix}\\b" + .replace("{TimePrefix}", TimePrefix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex5 = "\\b{BasicTime}((\\s*{DescRegex})(\\s+{TimePrefix})?|\\s+{TimePrefix})" + .replace("{TimePrefix}", TimePrefix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex6 = "{BasicTime}(\\s*{DescRegex})?\\s+{TimeSuffix}\\b" + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex7 = "\\b{TimeSuffix}\\s+[àa]\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffix}", TimeSuffix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex8 = "\\b{TimeSuffix}\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffix}", TimeSuffix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex9 = "\\b{PeriodHourNumRegex}\\s+{FivesRegex}((\\s*{DescRegex})|\\b)" + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{FivesRegex}", FivesRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex10 = "\\b{BaseDateTime.HourRegex}(\\s*h\\s*){BaseDateTime.MinuteRegex}(\\s*{DescRegex})?(\\s+{TimePrefix})?" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String HourRegex = "\\b{BaseDateTime.HourRegex}" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String PeriodDescRegex = "(?pm|am|p\\.m\\.|a\\.m\\.|p)"; + + public static final String PeriodPmRegex = "(?dans l'apr[eè]s-midi|ce soir|d[eu] soir|dans l[ea] soir[eé]e|dans la nuit|d[eu] soir[ée]e)s?"; + + public static final String PeriodAmRegex = "(?d[eu] matin|matin([ée]e)s?"; + + public static final String PureNumFromTo = "((du|depuis|des?)\\s+)?({HourRegex}|{PeriodHourNumRegex})(\\s*(?{PeriodDescRegex}))?\\s*{TillRegex}\\s*({HourRegex}|{PeriodHourNumRegex})\\s*(?{PmRegex}|{AmRegex}|{PeriodDescRegex})?" + .replace("{HourRegex}", HourRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{PeriodDescRegex}", PeriodDescRegex) + .replace("{TillRegex}", TillRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex); + + public static final String PureNumBetweenAnd = "(entre\\s+)({HourRegex}|{PeriodHourNumRegex})(\\s*(?{PeriodDescRegex}))?\\s*{RangeConnectorRegex}\\s*({HourRegex}|{PeriodHourNumRegex})\\s*(?{PmRegex}|{AmRegex}|{PeriodDescRegex})?" + .replace("{HourRegex}", HourRegex) + .replace("{PeriodHourNumRegex}", PeriodHourNumRegex) + .replace("{PeriodDescRegex}", PeriodDescRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex); + + public static final String SpecificTimeFromTo = "^[.]"; + + public static final String SpecificTimeBetweenAnd = "^[.]"; + + public static final String PrepositionRegex = "(?^([aà](\\s+?la)?|en|sur(\\s*l[ea])?|de)$)"; + + public static final String TimeOfDayRegex = "\\b(?((((dans\\s+(l[ea])?\\s+)?((?d[eé]but(\\s+|-)|t[oô]t(\\s+|-)(l[ea]\\s*)?)|(?fin\\s*|fin de(\\s+(la)?)|tard\\s*))?(matin([ée]e)?|((d|l)?'?)apr[eè]s[-|\\s*]midi|nuit|soir([eé]e)?)))|(((\\s+(l[ea])?\\s+)?)jour(n[eé]e)?))s?)\\b"; + + public static final String SpecificTimeOfDayRegex = "\\b(({RelativeRegex}\\s+{TimeOfDayRegex})|({TimeOfDayRegex}\\s*({NextSuffixRegex}))\\b|\\b(du )?soir)s?\\b" + .replace("{TimeOfDayRegex}", TimeOfDayRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String TimeFollowedUnit = "^\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String TimeNumberCombinedWithUnit = "\\b(?\\d+(\\.\\d*)?){TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String NowRegex = "\\b(?(ce\\s+)?moment|maintenant|d[eè]s que possible|dqp|r[eé]cemment|auparavant)\\b"; + + public static final String SuffixRegex = "^\\s*((dans\\s+l[ea]\\s+)|(en\\s+)|(d(u|\\'))?(matin([eé]e)?|apr[eè]s-midi|soir[eé]e|nuit))\\b"; + + public static final String DateTimeTimeOfDayRegex = "\\b(?matin([eé]e)?|apr[eè]s-midi|nuit|soir)\\b"; + + public static final String DateTimeSpecificTimeOfDayRegex = "\\b(({RelativeRegex}\\s+{DateTimeTimeOfDayRegex})\\b|\\b(ce(tte)?\\s+)(soir|nuit))\\b" + .replace("{DateTimeTimeOfDayRegex}", DateTimeTimeOfDayRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String TimeOfTodayAfterRegex = "^\\s*(,\\s*)?(en|dans|du\\s+)?{DateTimeSpecificTimeOfDayRegex}" + .replace("{DateTimeSpecificTimeOfDayRegex}", DateTimeSpecificTimeOfDayRegex); + + public static final String TimeOfTodayBeforeRegex = "{DateTimeSpecificTimeOfDayRegex}(\\s*,)?(\\s+([àa]|vers|pour))?\\s*$" + .replace("{DateTimeSpecificTimeOfDayRegex}", DateTimeSpecificTimeOfDayRegex); + + public static final String SimpleTimeOfTodayAfterRegex = "({HourNumRegex}|{BaseDateTime.HourRegex})\\s*(,\\s*)?(en|[àa]\\s+)?{DateTimeSpecificTimeOfDayRegex}" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DateTimeSpecificTimeOfDayRegex}", DateTimeSpecificTimeOfDayRegex); + + public static final String SimpleTimeOfTodayBeforeRegex = "{DateTimeSpecificTimeOfDayRegex}(\\s*,)?(\\s+([àa]|vers|pour))?\\s*({HourNumRegex}|{BaseDateTime.HourRegex})" + .replace("{DateTimeSpecificTimeOfDayRegex}", DateTimeSpecificTimeOfDayRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String SpecificEndOfRegex = "(la\\s+)?fin(\\s+de\\s*|\\s*de*l[ea])?\\s*$"; + + public static final String UnspecificEndOfRegex = "^[.]"; + + public static final String UnspecificEndOfRangeRegex = "^[.]"; + + public static final String PeriodTimeOfDayRegex = "\\b((dans\\s+(le)?\\s+)?((?d[eé]but(\\s+|-|d[ue]|de la)|t[oô]t)|(?tard\\s*|fin(\\s+|-|d[eu])?))?(?matin|((d|l)?'?)apr[eè]s-midi|nuit|soir([eé]e)?))\\b"; + + public static final String PeriodSpecificTimeOfDayRegex = "\\b(({RelativeRegex}\\s+{PeriodTimeOfDayRegex})\\b|\\b(ce(tte)?\\s+)(soir|nuit))\\b" + .replace("{PeriodTimeOfDayRegex}", PeriodTimeOfDayRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String PeriodTimeOfDayWithDateRegex = "\\b(({TimeOfDayRegex}))\\b" + .replace("{TimeOfDayRegex}", TimeOfDayRegex); + + public static final String LessThanRegex = "^[.]"; + + public static final String MoreThanRegex = "^[.]"; + + public static final String DurationUnitRegex = "(?ann[eé]es?|ans?|mois|semaines?|jours?|heures?|hrs?|h|minutes?|mins?|secondes?|secs?|journ[eé]e)\\b"; + + public static final String SuffixAndRegex = "(?\\s*(et)\\s+(une?\\s+)?(?demi|quart))"; + + public static final String PeriodicRegex = "\\b(?quotidien(ne)?|journellement|mensuel(le)?|jours?|hebdomadaire|bihebdomadaire|annuel(lement)?)\\b"; + + public static final String EachUnitRegex = "(?(chaque|toutes les|tous les)(?\\s+autres)?\\s*{DurationUnitRegex})" + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String EachPrefixRegex = "\\b(?(chaque|tous les|(toutes les))\\s*$)"; + + public static final String SetEachRegex = "\\b(?(chaque|tous les|(toutes les))\\s*)"; + + public static final String SetLastRegex = "(?prochain|dernier|derni[eè]re|pass[ée]s|pr[eé]c[eé]dent|courant|en\\s*cours)"; + + public static final String EachDayRegex = "^\\s*(chaque|tous les)\\s*(jour|jours)\\b"; + + public static final String DurationFollowedUnit = "^\\s*{SuffixAndRegex}?(\\s+|-)?{DurationUnitRegex}" + .replace("{SuffixAndRegex}", SuffixAndRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String NumberCombinedWithDurationUnit = "\\b(?\\d+(\\.\\d*)?)(-)?{DurationUnitRegex}" + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String AnUnitRegex = "\\b(((?demi\\s+)?(-)\\s+{DurationUnitRegex}))" + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String DuringRegex = "^[.]"; + + public static final String AllRegex = "\\b(?toute\\s(l['ea])\\s?(?ann[eé]e|mois|semaines?|jours?|journ[eé]e))\\b"; + + public static final String HalfRegex = "((une?\\s*)|\\b)(?demi?(\\s*|-)+(?ann[eé]e|ans?|mois|semaine|jour|heure))\\b"; + + public static final String ConjunctionRegex = "\\b((et(\\s+de|pour)?)|avec)\\b"; + + public static final String HolidayRegex1 = "\\b(?vendredi saint|mercredi des cendres|p[aâ]ques|l'action de gr[âa]ce|mardi gras|la saint-sylvestre|la saint sylvestre|la saint-valentin|la saint valentin|nouvel an chinois|nouvel an|r[eé]veillon de nouvel an|jour de l'an|premier-mai|ler-mai|1-mai|poisson d'avril|r[eé]veillon de no[eë]l|veille de no[eë]l|noël|noel|thanksgiving|halloween|yuandan)(\\s+((d[ue]\\s+|d'))?({YearRegex}|({ThisPrefixRegex}\\s+)ann[eé]e|ann[eé]e\\s+({PastSuffixRegex}|{NextSuffixRegex})))?\\b" + .replace("{YearRegex}", YearRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String HolidayRegex2 = "\\b(?martin luther king|martin luther king jr|toussaint|st patrick|st george|cinco de mayo|l'ind[eé]pendance(\\s+am[eé]ricaine)?|guy fawkes)(\\s+(de\\s+)?({YearRegex}|{ThisPrefixRegex}\\s+ann[eé]e|ann[eé]e\\s+({PastSuffixRegex}|{NextSuffixRegex})))?\\b" + .replace("{YearRegex}", YearRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String HolidayRegex3 = "(?(jour\\s*(d[eu]|des)\\s*(canberra|p[aâ]ques|colomb|bastille|la prise de la bastille|thanks\\s*giving|bapt[êe]me|nationale|d'armistice|inaugueration|marmotte|assomption|femme|comm[ée]moratif)))(\\s+(de\\s+)?({YearRegex}|{ThisPrefixRegex}\\s+ann[eé]e|ann[eé]e\\s+({PastSuffixRegex}|{NextSuffixRegex})))?" + .replace("{YearRegex}", YearRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String HolidayRegex4 = "(?(f[eê]te\\s*(d[eu]|des)\\s*)(travail|m[eè]res?|p[eè]res?))(\\s+(de\\s+)?({YearRegex}|{ThisPrefixRegex}\\s+ann[eé]e|ann[eé]e\\s+({PastSuffixRegex}|{NextSuffixRegex})))?\\b" + .replace("{YearRegex}", YearRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex); + + public static final String DateTokenPrefix = "le "; + + public static final String TimeTokenPrefix = "à "; + + public static final String TokenBeforeDate = "le "; + + public static final String TokenBeforeTime = "à "; + + public static final String AMTimeRegex = "(?matin([ée]e)?)"; + + public static final String PMTimeRegex = "\\b(?(d'|l')?apr[eè]s-midi|nuit|((\\s*ce|du)\\s+)?soir)\\b"; + + public static final String BeforeRegex = "\\b(avant)\\b"; + + public static final String BeforeRegex2 = "\\b(entre\\s*(le|la(s)?)?)\\b"; + + public static final String AfterRegex = "\\b(apres)\\b"; + + public static final String SinceRegex = "\\b(depuis)\\b"; + + public static final String AroundRegex = "\\b(vers)\\b"; + + public static final String AgoPrefixRegex = "\\b(y a)\\b"; + + public static final String LaterRegex = "\\b(plus tard)\\b"; + + public static final String AgoRegex = "^[.]"; + + public static final String BeforeAfterRegex = "^[.]"; + + public static final String InConnectorRegex = "\\b(dans|en|sur)\\b"; + + public static final String SinceYearSuffixRegex = "^[.]"; + + public static final String WithinNextPrefixRegex = "^[.]"; + + public static final String TodayNowRegex = "\\b(aujourd'hui|maintenant)\\b"; + + public static final String MorningStartEndRegex = "(^(matin))|((matin)$)"; + + public static final String AfternoonStartEndRegex = "(^((d'|l')?apr[eè]s-midi))|(((d'|l')?apr[eè]s-midi)$)"; + + public static final String EveningStartEndRegex = "(^(soir[ée]e|soir))|((soir[ée]e|soir)$)"; + + public static final String NightStartEndRegex = "(^(nuit))|((nuit)$)"; + + public static final String InexactNumberRegex = "\\b(quel qu[ée]s|quelqu[ée]s?|plusieurs?|divers)\\b"; + + public static final String InexactNumberUnitRegex = "({InexactNumberRegex})\\s+({DurationUnitRegex})" + .replace("{InexactNumberRegex}", InexactNumberRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String RelativeTimeUnitRegex = "(((({ThisPrefixRegex})?)\\s+({TimeUnitRegex}(\\s*{NextSuffixRegex}|{PastSuffixRegex})?))|((le))\\s+({RestrictedTimeUnitRegex}))" + .replace("{NextSuffixRegex}", NextSuffixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{TimeUnitRegex}", TimeUnitRegex) + .replace("{RestrictedTimeUnitRegex}", RestrictedTimeUnitRegex); + + public static final String RelativeDurationUnitRegex = "((\\b({DurationUnitRegex})(\\s+{NextSuffixRegex}|{PastSuffixRegex})?)|((le|my))\\s+({RestrictedTimeUnitRegex}))" + .replace("{NextSuffixRegex}", NextSuffixRegex) + .replace("{PastSuffixRegex}", PastSuffixRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex) + .replace("{RestrictedTimeUnitRegex}", RestrictedTimeUnitRegex); + + public static final String ReferenceDatePeriodRegex = "^[.]"; + + public static final String UpcomingPrefixRegex = ".^"; + + public static final String NextPrefixRegex = ".^"; + + public static final String PastPrefixRegex = ".^"; + + public static final String PreviousPrefixRegex = ".^"; + + public static final String RelativeDayRegex = "\\b(((la\\s+)?{RelativeRegex}\\s+journ[ée]e))\\b" + .replace("{RelativeRegex}", RelativeRegex); + + public static final String ConnectorRegex = "^(,|pour|t|vers|le)$"; + + public static final String ConnectorAndRegex = "\\b(et\\s*(le|las?)?)\\b.+"; + + public static final String FromRegex = "((de|du)?)$"; + + public static final String FromRegex2 = "((depuis|de)(\\s*las?)?)$"; + + public static final String FromToRegex = "\\b(du|depuis|des?).+(au|à|a)\\b.+"; + + public static final String SingleAmbiguousMonthRegex = "^(le\\s+)?(may|march)$"; + + public static final String UnspecificDatePeriodRegex = "^[.]"; + + public static final String PrepositionSuffixRegex = "\\b(du|de|[àa]|vers|dans)$"; + + public static final String FlexibleDayRegex = "(?([A-Za-z]+\\s)?[A-Za-z\\d]+)"; + + public static final String ForTheRegex = "\\b(((pour le {FlexibleDayRegex})|(dans (le\\s+)?{FlexibleDayRegex}(?<=(st|nd|rd|th))))(?\\s*(,|\\.|!|\\?|$)))" + .replace("{FlexibleDayRegex}", FlexibleDayRegex); + + public static final String WeekDayAndDayOfMonthRegex = "\\b{WeekDayRegex}\\s+(le\\s+{FlexibleDayRegex})\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{FlexibleDayRegex}", FlexibleDayRegex); + + public static final String WeekDayAndDayRegex = "\\b{WeekDayRegex}\\s+(?!(the)){DayRegex}(?!([-:]|(\\s+({AmDescRegex}|{PmDescRegex}|{OclockRegex}))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DayRegex}", DayRegex) + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String RestOfDateRegex = "\\b(reste|fin)\\s+(d[eu]\\s+)?((le|ce(tte)?)\\s+)?(?semaine|mois|l'ann[ée]e)\\b"; + + public static final String RestOfDateTimeRegex = "\\b(reste|fin)\\s+(d[eu]\\s+)?((le|ce(tte)?)\\s+)?(?jour)\\b"; + + public static final String LaterEarlyPeriodRegex = "^[.]"; + + public static final String WeekWithWeekDayRangeRegex = "^[.]"; + + public static final String GeneralEndingRegex = "^[.]"; + + public static final String MiddlePauseRegex = "^[.]"; + + public static final String DurationConnectorRegex = "^[.]"; + + public static final String PrefixArticleRegex = "^[\\.]"; + + public static final String OrRegex = "^[.]"; + + public static final String YearPlusNumberRegex = "^[.]"; + + public static final String NumberAsTimeRegex = "^[.]"; + + public static final String TimeBeforeAfterRegex = "^[.]"; + + public static final String DateNumberConnectorRegex = "^[.]"; + + public static final String CenturyRegex = "^[.]"; + + public static final String DecadeRegex = "^[.]"; + + public static final String DecadeWithCenturyRegex = "^[.]"; + + public static final String RelativeDecadeRegex = "^[.]"; + + public static final String YearSuffix = "(,?\\s*({DateYearRegex}|{FullTextYearRegex}))" + .replace("{DateYearRegex}", DateYearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String SuffixAfterRegex = "^[.]"; + + public static final String YearPeriodRegex = "^[.]"; + + public static final String FutureSuffixRegex = "^[.]"; + + public static final String ComplexDatePeriodRegex = "^[.]"; + + public static final String AmbiguousPointRangeRegex = "^(mar\\.?)$"; + + public static final ImmutableMap UnitMap = ImmutableMap.builder() + .put("annees", "Y") + .put("annee", "Y") + .put("an", "Y") + .put("ans", "Y") + .put("mois", "MON") + .put("semaines", "W") + .put("semaine", "W") + .put("journees", "D") + .put("journee", "D") + .put("jour", "D") + .put("jours", "D") + .put("heures", "H") + .put("heure", "H") + .put("hrs", "H") + .put("hr", "H") + .put("h", "H") + .put("minutes", "M") + .put("minute", "M") + .put("mins", "M") + .put("min", "M") + .put("secondes", "S") + .put("seconde", "S") + .put("secs", "S") + .put("sec", "S") + .build(); + + public static final ImmutableMap UnitValueMap = ImmutableMap.builder() + .put("annees", 31536000L) + .put("annee", 31536000L) + .put("l'annees", 31536000L) + .put("l'annee", 31536000L) + .put("an", 31536000L) + .put("ans", 31536000L) + .put("mois", 2592000L) + .put("semaines", 604800L) + .put("semaine", 604800L) + .put("journees", 86400L) + .put("journee", 86400L) + .put("jour", 86400L) + .put("jours", 86400L) + .put("heures", 3600L) + .put("heure", 3600L) + .put("hrs", 3600L) + .put("hr", 3600L) + .put("h", 3600L) + .put("minutes", 60L) + .put("minute", 60L) + .put("mins", 60L) + .put("min", 60L) + .put("secondes", 1L) + .put("seconde", 1L) + .put("secs", 1L) + .put("sec", 1L) + .build(); + + public static final ImmutableMap SpecialYearPrefixesMap = ImmutableMap.builder() + .put("", "") + .build(); + + public static final ImmutableMap SeasonMap = ImmutableMap.builder() + .put("printemps", "SP") + .put("été", "SU") + .put("automne", "FA") + .put("hiver", "WI") + .build(); + + public static final ImmutableMap SeasonValueMap = ImmutableMap.builder() + .put("SP", 3) + .put("SU", 6) + .put("FA", 9) + .put("WI", 12) + .build(); + + public static final ImmutableMap CardinalMap = ImmutableMap.builder() + .put("premier", 1) + .put("1er", 1) + .put("deuxième", 2) + .put("2e", 2) + .put("troisième", 3) + .put("troisieme", 3) + .put("3e", 3) + .put("quatrième", 4) + .put("4e", 4) + .put("cinqième", 5) + .put("5e", 5) + .build(); + + public static final ImmutableMap DayOfWeek = ImmutableMap.builder() + .put("lundi", 1) + .put("mardi", 2) + .put("mercredi", 3) + .put("jeudi", 4) + .put("vendredi", 5) + .put("samedi", 6) + .put("dimanche", 0) + .put("lun", 1) + .put("mar", 2) + .put("mer", 3) + .put("jeu", 4) + .put("ven", 5) + .put("sam", 6) + .put("dim", 0) + .put("lun.", 1) + .put("mar.", 2) + .put("mer.", 3) + .put("jeu.", 4) + .put("ven.", 5) + .put("sam.", 6) + .put("dim.", 0) + .build(); + + public static final ImmutableMap MonthOfYear = ImmutableMap.builder() + .put("janvier", 1) + .put("fevrier", 2) + .put("février", 2) + .put("mars", 3) + .put("mar", 3) + .put("mar.", 3) + .put("avril", 4) + .put("avr", 4) + .put("avr.", 4) + .put("mai", 5) + .put("juin", 6) + .put("jun", 6) + .put("jun.", 6) + .put("juillet", 7) + .put("aout", 8) + .put("août", 8) + .put("septembre", 9) + .put("octobre", 10) + .put("novembre", 11) + .put("decembre", 12) + .put("décembre", 12) + .put("janv", 1) + .put("janv.", 1) + .put("jan", 1) + .put("jan.", 1) + .put("fevr", 2) + .put("fevr.", 2) + .put("févr.", 2) + .put("févr", 2) + .put("fev", 2) + .put("fev.", 2) + .put("juil", 7) + .put("jul", 7) + .put("jul.", 7) + .put("sep", 9) + .put("sep.", 9) + .put("sept.", 9) + .put("sept", 9) + .put("oct", 10) + .put("oct.", 10) + .put("nov", 11) + .put("nov.", 11) + .put("dec", 12) + .put("dec.", 12) + .put("déc.", 12) + .put("déc", 12) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .build(); + + public static final ImmutableMap Numbers = ImmutableMap.builder() + .put("zero", 0) + .put("un", 1) + .put("une", 1) + .put("a", 1) + .put("deux", 2) + .put("trois", 3) + .put("quatre", 4) + .put("cinq", 5) + .put("six", 6) + .put("sept", 7) + .put("huit", 8) + .put("neuf", 9) + .put("dix", 10) + .put("onze", 11) + .put("douze", 12) + .put("treize", 13) + .put("quatorze", 14) + .put("quinze", 15) + .put("seize", 16) + .put("dix-sept", 17) + .put("dix-huit", 18) + .put("dix-neuf", 19) + .put("vingt-et-un", 21) + .put("vingt et un", 21) + .put("vingt", 20) + .put("vingt deux", 22) + .put("vingt-deux", 22) + .put("vingt trois", 23) + .put("vingt-trois", 23) + .put("vingt quatre", 24) + .put("vingt-quatre", 24) + .put("vingt cinq", 25) + .put("vingt-cinq", 25) + .put("vingt six", 26) + .put("vingt-six", 26) + .put("vingt sept", 27) + .put("vingt-sept", 27) + .put("vingt huit", 28) + .put("vingt-huit", 28) + .put("vingt neuf", 29) + .put("vingt-neuf", 29) + .put("trente", 30) + .put("trente et un", 31) + .put("trente-et-un", 31) + .put("trente deux", 32) + .put("trente-deux", 32) + .put("trente trois", 33) + .put("trente-trois", 33) + .put("trente quatre", 34) + .put("trente-quatre", 34) + .put("trente cinq", 35) + .put("trente-cinq", 35) + .put("trente six", 36) + .put("trente-six", 36) + .put("trente sept", 37) + .put("trente-sept", 37) + .put("trente huit", 38) + .put("trente-huit", 38) + .put("trente neuf", 39) + .put("trente-neuf", 39) + .put("quarante", 40) + .put("quarante et un", 41) + .put("quarante-et-un", 41) + .put("quarante deux", 42) + .put("quarante-duex", 42) + .put("quarante trois", 43) + .put("quarante-trois", 43) + .put("quarante quatre", 44) + .put("quarante-quatre", 44) + .put("quarante cinq", 45) + .put("quarante-cinq", 45) + .put("quarante six", 46) + .put("quarante-six", 46) + .put("quarante sept", 47) + .put("quarante-sept", 47) + .put("quarante huit", 48) + .put("quarante-huit", 48) + .put("quarante neuf", 49) + .put("quarante-neuf", 49) + .put("cinquante", 50) + .put("cinquante et un", 51) + .put("cinquante-et-un", 51) + .put("cinquante deux", 52) + .put("cinquante-deux", 52) + .put("cinquante trois", 53) + .put("cinquante-trois", 53) + .put("cinquante quatre", 54) + .put("cinquante-quatre", 54) + .put("cinquante cinq", 55) + .put("cinquante-cinq", 55) + .put("cinquante six", 56) + .put("cinquante-six", 56) + .put("cinquante sept", 57) + .put("cinquante-sept", 57) + .put("cinquante huit", 58) + .put("cinquante-huit", 58) + .put("cinquante neuf", 59) + .put("cinquante-neuf", 59) + .put("soixante", 60) + .put("soixante et un", 61) + .put("soixante-et-un", 61) + .put("soixante deux", 62) + .put("soixante-deux", 62) + .put("soixante trois", 63) + .put("soixante-trois", 63) + .put("soixante quatre", 64) + .put("soixante-quatre", 64) + .put("soixante cinq", 65) + .put("soixante-cinq", 65) + .put("soixante six", 66) + .put("soixante-six", 66) + .put("soixante sept", 67) + .put("soixante-sept", 67) + .put("soixante huit", 68) + .put("soixante-huit", 68) + .put("soixante neuf", 69) + .put("soixante-neuf", 69) + .put("soixante dix", 70) + .put("soixante-dix", 70) + .put("soixante et onze", 71) + .put("soixante-et-onze", 71) + .put("soixante douze", 72) + .put("soixante-douze", 72) + .put("soixante treize", 73) + .put("soixante-treize", 73) + .put("soixante quatorze", 74) + .put("soixante-quatorze", 74) + .put("soixante quinze", 75) + .put("soixante-quinze", 75) + .put("soixante seize", 76) + .put("soixante-seize", 76) + .put("soixante dix sept", 77) + .put("soixante-dix-sept", 77) + .put("soixante dix huit", 78) + .put("soixante-dix-huit", 78) + .put("soixante dix neuf", 79) + .put("soixante-dix-neuf", 79) + .put("quatre vingt", 80) + .put("quatre-vingt", 80) + .put("quatre vingt un", 81) + .put("quatre-vingt-un", 81) + .put("quatre vingt deux", 82) + .put("quatre-vingt-duex", 82) + .put("quatre vingt trois", 83) + .put("quatre-vingt-trois", 83) + .put("quatre vingt quatre", 84) + .put("quatre-vingt-quatre", 84) + .put("quatre vingt cinq", 85) + .put("quatre-vingt-cinq", 85) + .put("quatre vingt six", 86) + .put("quatre-vingt-six", 86) + .put("quatre vingt sept", 87) + .put("quatre-vingt-sept", 87) + .put("quatre vingt huit", 88) + .put("quatre-vingt-huit", 88) + .put("quatre vingt neuf", 89) + .put("quatre-vingt-neuf", 89) + .put("quatre vingt dix", 90) + .put("quatre-vingt-dix", 90) + .put("quatre vingt onze", 91) + .put("quatre-vingt-onze", 91) + .put("quatre vingt douze", 92) + .put("quatre-vingt-douze", 92) + .put("quatre vingt treize", 93) + .put("quatre-vingt-treize", 93) + .put("quatre vingt quatorze", 94) + .put("quatre-vingt-quatorze", 94) + .put("quatre vingt quinze", 95) + .put("quatre-vingt-quinze", 95) + .put("quatre vingt seize", 96) + .put("quatre-vingt-seize", 96) + .put("quatre vingt dix sept", 97) + .put("quatre-vingt-dix-sept", 97) + .put("quatre vingt dix huit", 98) + .put("quatre-vingt-dix-huit", 98) + .put("quatre vingt dix neuf", 99) + .put("quatre-vingt-dix-neuf", 99) + .put("cent", 100) + .build(); + + public static final ImmutableMap DayOfMonth = ImmutableMap.builder() + .put("1er", 1) + .put("2e", 2) + .put("3e", 3) + .put("4e", 4) + .put("5e", 5) + .put("6e", 6) + .put("7e", 7) + .put("8e", 8) + .put("9e", 9) + .put("10e", 10) + .put("11e", 11) + .put("12e", 12) + .put("13e", 13) + .put("14e", 14) + .put("15e", 15) + .put("16e", 16) + .put("17e", 17) + .put("18e", 18) + .put("19e", 19) + .put("20e", 20) + .put("21e", 21) + .put("22e", 22) + .put("23e", 23) + .put("24e", 24) + .put("25e", 25) + .put("26e", 26) + .put("27e", 27) + .put("28e", 28) + .put("29e", 29) + .put("30e", 30) + .put("31e", 31) + .build(); + + public static final ImmutableMap DoubleNumbers = ImmutableMap.builder() + .put("demi", 0.5D) + .put("quart", 0.25D) + .build(); + + public static final ImmutableMap HolidayNames = ImmutableMap.builder() + .put("fathers", new String[]{"peres", "pères", "fêtedespères", "fetedesperes"}) + .put("mothers", new String[]{"fêtedesmères", "fetedesmeres"}) + .put("thanksgiving", new String[]{"lactiondegrace", "lactiondegrâce", "jourdethanksgiving", "thanksgiving"}) + .put("martinlutherking", new String[]{"journeemartinlutherking", "martinlutherkingjr"}) + .put("washingtonsbirthday", new String[]{"washingtonsbirthday", "washingtonbirthday"}) + .put("canberra", new String[]{"canberraday"}) + .put("labour", new String[]{"fetedetravail", "travail", "fetedutravail"}) + .put("columbus", new String[]{"columbusday"}) + .put("memorial", new String[]{"jourcommémoratif", "jourcommemoratif"}) + .put("yuandan", new String[]{"yuandan", "nouvelanchinois"}) + .put("maosbirthday", new String[]{"maosbirthday"}) + .put("teachersday", new String[]{"teachersday", "teacherday"}) + .put("singleday", new String[]{"singleday"}) + .put("allsaintsday", new String[]{"allsaintsday"}) + .put("youthday", new String[]{"youthday"}) + .put("childrenday", new String[]{"childrenday", "childday"}) + .put("femaleday", new String[]{"femaleday"}) + .put("treeplantingday", new String[]{"treeplantingday"}) + .put("arborday", new String[]{"arborday"}) + .put("girlsday", new String[]{"girlsday"}) + .put("whiteloverday", new String[]{"whiteloverday"}) + .put("loverday", new String[]{"loverday"}) + .put("christmas", new String[]{"noel", "noël"}) + .put("xmas", new String[]{"xmas"}) + .put("newyear", new String[]{"nouvellesannees", "nouvelan"}) + .put("newyearday", new String[]{"jourdunouvelan"}) + .put("newyearsday", new String[]{"jourdel'an", "jourpremierdelannee", "jourpremierdelannée"}) + .put("inaugurationday", new String[]{"jourd'inaugueration", "inaugueration"}) + .put("groundhougday", new String[]{"marmotte"}) + .put("valentinesday", new String[]{"lasaint-valentin", "lasaintvalentin"}) + .put("stpatrickday", new String[]{"stpatrickday"}) + .put("aprilfools", new String[]{"poissond'avril"}) + .put("stgeorgeday", new String[]{"stgeorgeday"}) + .put("mayday", new String[]{"premier-mai", "ler-mai", "1-mai"}) + .put("cincodemayoday", new String[]{"cincodemayo"}) + .put("baptisteday", new String[]{"bapteme", "baptême"}) + .put("usindependenceday", new String[]{"l'independanceamericaine", "lindépendanceaméricaine"}) + .put("independenceday", new String[]{"l'indépendance", "lindependance"}) + .put("bastilleday", new String[]{"laprisedelabastille", "bastille"}) + .put("halloweenday", new String[]{"halloween"}) + .put("allhallowday", new String[]{"allhallowday"}) + .put("allsoulsday", new String[]{"allsoulsday"}) + .put("guyfawkesday", new String[]{"guyfawkesday"}) + .put("veteransday", new String[]{"veteransday"}) + .put("christmaseve", new String[]{"reveillondenoel", "réveillondenoël", "veilledenoel", "veilledenoël"}) + .put("newyeareve", new String[]{"réveillondenouvelan", "reveillondenouvelan", "lasaint-sylvestre", "lasaintsylvestre"}) + .build(); + + public static final String NightRegex = "\\b(minuit|nuit)\\b"; + + public static final ImmutableMap WrittenDecades = ImmutableMap.builder() + .put("", 0) + .build(); + + public static final ImmutableMap SpecialDecadeCases = ImmutableMap.builder() + .put("", 0) + .build(); + + public static final String DefaultLanguageFallback = "DMY"; + + public static final List DurationDateRestrictions = Arrays.asList(); + + public static final ImmutableMap AmbiguityFiltersDict = ImmutableMap.builder() + .put("^([eé]t[eé])$", "(? AmbiguityTimeFiltersDict = ImmutableMap.builder() + .put("heures?$", "\\b(pour|durée\\s+de|pendant)\\s+(\\S+\\s+){1,2}heures?\\b") + .build(); + + public static final List MorningTermList = Arrays.asList("matinee", "matin", "matinée"); + + public static final List AfternoonTermList = Arrays.asList("apres-midi", "apres midi", "après midi", "après-midi"); + + public static final List EveningTermList = Arrays.asList("soir", "soiree", "soirée"); + + public static final List DaytimeTermList = Arrays.asList("jour", "journee", "journée"); + + public static final List NightTermList = Arrays.asList("nuit"); + + public static final List SameDayTerms = Arrays.asList("aujourd'hui", "auj"); + + public static final List PlusOneDayTerms = Arrays.asList("demain", "a2m1", "lendemain", "jour suivant"); + + public static final List MinusOneDayTerms = Arrays.asList("hier", "dernier"); + + public static final List PlusTwoDayTerms = Arrays.asList("après demain", "après-demain", "apres-demain"); + + public static final List MinusTwoDayTerms = Arrays.asList("avant-hier", "avant hier"); + + public static final List FutureStartTerms = Arrays.asList("cette"); + + public static final List FutureEndTerms = Arrays.asList("prochaine", "prochain"); + + public static final List LastCardinalTerms = Arrays.asList("dernières", "dernière", "dernieres", "derniere", "dernier"); + + public static final List MonthTerms = Arrays.asList("mois"); + + public static final List MonthToDateTerms = Arrays.asList("mois à ce jour"); + + public static final List WeekendTerms = Arrays.asList("fin de semaine", "le weekend"); + + public static final List WeekTerms = Arrays.asList("semaine"); + + public static final List YearTerms = Arrays.asList("années", "ans", "an", "l'annees", "l'annee"); + + public static final List YearToDateTerms = Arrays.asList("année à ce jour", "an à ce jour"); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/PortugueseDateTime.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/PortugueseDateTime.java new file mode 100644 index 000000000..7f5fbdbfd --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/PortugueseDateTime.java @@ -0,0 +1,997 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class PortugueseDateTime { + + public static final String LangMarker = "Por"; + + public static final Boolean CheckBothBeforeAfter = false; + + public static final String TillRegex = "(?\\b(at[eé]h?|[aà]s|ao?)\\b|--|-|—|——)(\\s+\\b(o|[aà](s)?)\\b)?"; + + public static final String RangeConnectorRegex = "(?(e\\s*(([àa]s?)|o)?)|{BaseDateTime.RangeConnectorSymbolRegex})" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String DayRegex = "(?(?:3[0-1]|[1-2]\\d|0?[1-9]))(?=\\b|t)"; + + public static final String MonthNumRegex = "(?1[0-2]|(0)?[1-9])\\b"; + + public static final String AmDescRegex = "({BaseDateTime.BaseAmDescRegex})" + .replace("{BaseDateTime.BaseAmDescRegex}", BaseDateTime.BaseAmDescRegex); + + public static final String PmDescRegex = "({BaseDateTime.BasePmDescRegex})" + .replace("{BaseDateTime.BasePmDescRegex}", BaseDateTime.BasePmDescRegex); + + public static final String AmPmDescRegex = "({BaseDateTime.BaseAmPmDescRegex})" + .replace("{BaseDateTime.BaseAmPmDescRegex}", BaseDateTime.BaseAmPmDescRegex); + + public static final String DescRegex = "(?({AmDescRegex}|{PmDescRegex}))" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex); + + public static final String OclockRegex = "(?em\\s+ponto)"; + + public static final String OfPrepositionRegex = "(\\bd(o|a|e)s?\\b)"; + + public static final String AfterNextSuffixRegex = "\\b(que\\s+vem|passad[oa])\\b"; + + public static final String RangePrefixRegex = "((de(sde)?|das?|entre)\\s+(a(s)?\\s+)?)"; + + public static final String TwoDigitYearRegex = "\\b(?([0-9]\\d))(?!(\\s*((\\:\\d)|{AmDescRegex}|{PmDescRegex}|\\.\\d)))\\b" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex); + + public static final String RelativeRegex = "(?((est[ae]|pr[oó]xim[oa]|([uú]ltim(o|as|os)))(\\s+fina(l|is)\\s+d[eao])?)|(fina(l|is)\\s+d[eao]))\\b"; + + public static final String StrictRelativeRegex = "(?((est[ae]|pr[oó]xim[oa]|([uú]ltim(o|as|os)))(\\s+fina(l|is)\\s+d[eao])?)|(fina(l|is)\\s+d[eao]))\\b"; + + public static final String WrittenOneToNineRegex = "(uma?|dois|duas|tr[eê]s|quatro|cinco|seis|sete|oito|nove)"; + + public static final String WrittenOneHundredToNineHundredRegex = "(duzent[oa]s|trezent[oa]s|[cq]uatrocent[ao]s|quinhent[ao]s|seiscent[ao]s|setecent[ao]s|oitocent[ao]s|novecent[ao]s|cem|(?((dois\\s+)?mil)((\\s+e)?\\s+{WrittenOneHundredToNineHundredRegex})?((\\s+e)?\\s+{WrittenOneToNinetyNineRegex})?)" + .replace("{WrittenOneToNinetyNineRegex}", WrittenOneToNinetyNineRegex) + .replace("{WrittenOneHundredToNineHundredRegex}", WrittenOneHundredToNineHundredRegex); + + public static final String YearRegex = "({BaseDateTime.FourDigitYearRegex}|{FullTextYearRegex})" + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String RelativeMonthRegex = "(?([nd]?es[st]e|pr[óo]ximo|passsado|[uú]ltimo)\\s+m[eê]s)\\b"; + + public static final String MonthRegex = "(?abr(il)?|ago(sto)?|dez(embro)?|fev(ereiro)?|jan(eiro)?|ju[ln](ho)?|mar([çc]o)?|maio?|nov(embro)?|out(ubro)?|sep?t(embro)?)"; + + public static final String MonthSuffixRegex = "(?((em|no)\\s+|d[eo]\\s+)?({RelativeMonthRegex}|{MonthRegex}))" + .replace("{RelativeMonthRegex}", RelativeMonthRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String DateUnitRegex = "(?anos?|meses|m[êe]s|semanas?|dias?)\\b"; + + public static final String PastRegex = "(?\\b(passad[ao](s)?|[uú]ltim[oa](s)?|anterior(es)?|h[aá]|pr[ée]vi[oa](s)?)\\b)"; + + public static final String FutureRegex = "(?\\b(seguinte(s)?|pr[oó]xim[oa](s)?|dentro\\s+de|em|daqui\\s+a)\\b)"; + + public static final String SimpleCasesRegex = "\\b((desde\\s+[oa]|desde|d[oa])\\s+)?(dia\\s+)?({DayRegex})\\s*{TillRegex}\\s*(o dia\\s+)?({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthFrontSimpleCasesRegex = "\\b{MonthSuffixRegex}\\s+((desde\\s+[oa]|desde|d[oa])\\s+)?(dia\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthFrontBetweenRegex = "\\b{MonthSuffixRegex}\\s+((entre|entre\\s+[oa]s?)\\s+)(dias?\\s+)?({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String DayBetweenRegex = "\\b((entre|entre\\s+[oa]s?)\\s+)(dia\\s+)?({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*){YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String OneWordPeriodRegex = "\\b(((pr[oó]xim[oa]?|[nd]?es[st]e|aquel[ea]|[uú]ltim[oa]?|em)\\s+)?(?abr(il)?|ago(sto)?|dez(embro)?|fev(ereiro)?|jan(eiro)?|ju[ln](ho)?|mar([çc]o)?|maio?|nov(embro)?|out(ubro)?|sep?t(embro)?)|(?<=\\b(de|do|da|o|a)\\s+)?(pr[oó]xim[oa](s)?|[uú]ltim[oa]s?|est(e|a))\\s+(fim de semana|fins de semana|semana|m[êe]s|ano)|fim de semana|fins de semana|(m[êe]s|anos)? [àa] data)\\b"; + + public static final String MonthWithYearRegex = "\\b(((pr[oó]xim[oa](s)?|[nd]?es[st]e|aquele|[uú]ltim[oa]?|em)\\s+)?(?abr(il)?|ago(sto)?|dez(embro)?|fev(ereiro)?|jan(eiro)?|ju[ln](ho)?|mar([çc]o)?|maio?|nov(embro)?|out(ubro)?|sep?t(embro)?)\\s+((de|do|da|o|a)\\s+)?({YearRegex}|{TwoDigitYearRegex}|(?pr[oó]ximo(s)?|[uú]ltimo?|[nd]?es[st]e)\\s+ano))\\b" + .replace("{YearRegex}", YearRegex) + .replace("{TwoDigitYearRegex}", TwoDigitYearRegex); + + public static final String MonthNumWithYearRegex = "({YearRegex}(\\s*?)[/\\-\\.](\\s*?){MonthNumRegex})|({MonthNumRegex}(\\s*?)[/\\-](\\s*?){YearRegex})" + .replace("{YearRegex}", YearRegex) + .replace("{MonthNumRegex}", MonthNumRegex); + + public static final String WeekOfMonthRegex = "(?(a|na\\s+)?(?primeira?|1a|segunda|2a|terceira|3a|[qc]uarta|4a|quinta|5a|[uú]ltima)\\s+semana\\s+{MonthSuffixRegex})" + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String WeekOfYearRegex = "(?(a|na\\s+)?(?primeira?|1a|segunda|2a|terceira|3a|[qc]uarta|4a|quinta|5a|[uú]ltima?)\\s+semana(\\s+d[oe]?)?\\s+({YearRegex}|(?pr[oó]ximo|[uú]ltimo|[nd]?es[st]e)\\s+ano))" + .replace("{YearRegex}", YearRegex); + + public static final String FollowedDateUnit = "^\\s*{DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String NumberCombinedWithDateUnit = "\\b(?\\d+(\\.\\d*)?){DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String QuarterRegex = "(n?o\\s+)?(?primeiro|1[oº]|segundo|2[oº]|terceiro|3[oº]|[qc]uarto|4[oº])\\s+trimestre(\\s+d[oe]|\\s*,\\s*)?\\s+({YearRegex}|(?pr[oó]ximo(s)?|[uú]ltimo?|[nd]?es[st]e)\\s+ano)" + .replace("{YearRegex}", YearRegex); + + public static final String QuarterRegexYearFront = "({YearRegex}|(?pr[oó]ximo(s)?|[uú]ltimo?|[nd]?es[st]e)\\s+ano)\\s+(n?o\\s+)?(?(primeiro)|1[oº]|segundo|2[oº]|terceiro|3[oº]|[qc]uarto|4[oº])\\s+trimestre" + .replace("{YearRegex}", YearRegex); + + public static final String AllHalfYearRegex = "^[.]"; + + public static final String PrefixDayRegex = "^[.]"; + + public static final String SeasonRegex = "\\b(?(([uú]ltim[oa]|[nd]?es[st][ea]|n?[oa]|(pr[oó]xim[oa]s?|seguinte))\\s+)?(?primavera|ver[ãa]o|outono|inverno)((\\s+)?(seguinte|((de\\s+|,)?\\s*{YearRegex})|((do\\s+)?(?pr[oó]ximo|[uú]ltimo|[nd]?es[st]e)\\s+ano)))?)\\b" + .replace("{YearRegex}", YearRegex); + + public static final String WhichWeekRegex = "\\b(semana)(\\s*)(?5[0-3]|[1-4]\\d|0?[1-9])\\b"; + + public static final String WeekOfRegex = "(semana)(\\s*)((do|da|de))"; + + public static final String MonthOfRegex = "(mes)(\\s*)((do|da|de))"; + + public static final String RangeUnitRegex = "\\b(?anos?|meses|m[êe]s|semanas?)\\b"; + + public static final String BeforeAfterRegex = "^[.]"; + + public static final String InConnectorRegex = "\\b(em)\\b"; + + public static final String SinceYearSuffixRegex = "^[.]"; + + public static final String WithinNextPrefixRegex = "^[.]"; + + public static final String TodayNowRegex = "\\b(hoje|agora)\\b"; + + public static final String CenturySuffixRegex = "^[.]"; + + public static final String FromRegex = "((desde|de)(\\s*a(s)?)?)$"; + + public static final String BetweenRegex = "(entre\\s*([oa](s)?)?)"; + + public static final String WeekDayRegex = "\\b(?(domingos?|(segunda|ter[çc]a|quarta|quinta|sexta)s?([-\\s+]feiras?)?|s[aá]bados?|(2|3|4|5|6)[aª])\\b|(dom|seg|ter[cç]|qua|qui|sex|sab)\\b(\\.?(?=\\s|,|;|$)))"; + + public static final String OnRegex = "(?<=\\b(em|no)\\s+)({DayRegex}s?)\\b" + .replace("{DayRegex}", DayRegex); + + public static final String RelaxedOnRegex = "(?<=\\b(em|n[oa]|d[oa])\\s+)(dia\\s+)?({DayRegex}s?)\\b(?!\\s*[/\\\\\\-\\.,:\\s]\\s*(\\d|{MonthRegex}))" + .replace("{DayRegex}", DayRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String ThisRegex = "\\b(([nd]?es[st][ea]\\s*){WeekDayRegex})|({WeekDayRegex}\\s*([nd]?es[st]a\\s+semana))\\b" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String LastDateRegex = "\\b(([uú]ltim[ao])\\s*{WeekDayRegex})|({WeekDayRegex}(\\s+(([nd]?es[st]a|na|da)\\s+([uú]ltima\\s+)?semana)))\\b" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String NextDateRegex = "\\b(((pr[oó]xim[oa]|seguinte)\\s*){WeekDayRegex})|({WeekDayRegex}((\\s+(pr[oó]xim[oa]|seguinte))|(\\s+(da\\s+)?(semana\\s+seguinte|pr[oó]xima\\s+semana))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String SpecialDayRegex = "\\b((d?o\\s+)?(dia\\s+antes\\s+de\\s+ontem|antes\\s+de\\s+ontem|anteontem)|((d?o\\s+)?(dia\\s+|depois\\s+|dia\\s+depois\\s+)?de\\s+amanh[aã])|(o\\s)?dia\\s+seguinte|(o\\s)?pr[oó]ximo\\s+dia|(o\\s+)?[uú]ltimo\\s+dia|ontem|amanh[ãa]|hoje)|(do\\s+dia$)\\b"; + + public static final String SpecialDayWithNumRegex = "^[.]"; + + public static final String ForTheRegex = ".^"; + + public static final String WeekDayAndDayOfMonthRegex = ".^"; + + public static final String WeekDayAndDayRegex = "\\b{WeekDayRegex}\\s+({DayRegex})(?!([-:/]|\\.\\d|(\\s+({AmDescRegex}|{PmDescRegex}|{OclockRegex}))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DayRegex}", DayRegex) + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String WeekDayOfMonthRegex = "(?(n?[ao]\\s+)?(?primeir[ao]|1[ao]|segund[ao]|2[ao]|terceir[ao]|3[ao]|[qc]uart[ao]|4[ao]|quint[ao]|5[ao]|[uú]ltim[ao])\\s+{WeekDayRegex}\\s+{MonthSuffixRegex})" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String RelativeWeekDayRegex = "^[.]"; + + public static final String AmbiguousRangeModifierPrefix = "^[.]"; + + public static final String NumberEndingPattern = "^[.]"; + + public static final String SpecialDateRegex = "(?<=\\bno\\s+){DayRegex}\\b" + .replace("{DayRegex}", DayRegex); + + public static final String OfMonthRegex = "^\\s*de\\s*{MonthSuffixRegex}" + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String MonthEndRegex = "({MonthRegex}\\s*(o)?\\s*$)" + .replace("{MonthRegex}", MonthRegex); + + public static final String WeekDayEnd = "{WeekDayRegex}\\s*,?\\s*$" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String WeekDayStart = "^[\\.]"; + + public static final String DateYearRegex = "(?{YearRegex}|{TwoDigitYearRegex})" + .replace("{YearRegex}", YearRegex) + .replace("{TwoDigitYearRegex}", TwoDigitYearRegex); + + public static final String DateExtractor1 = "\\b({WeekDayRegex}(\\s+|\\s*,\\s*))?{DayRegex}((\\s*(de)|[/\\\\\\.\\- ])\\s*)?{MonthRegex}\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DayRegex}", DayRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String DateExtractor2 = "\\b({WeekDayRegex}(\\s+|\\s*,\\s*))?({DayRegex}\\s*([/\\.\\-]|de)?\\s*{MonthRegex}(\\s*([,./-]|de)\\s*){DateYearRegex}|{BaseDateTime.FourDigitYearRegex}\\s*[/\\.\\- ]\\s*{DayRegex}\\s*[/\\.\\- ]\\s*{MonthRegex})\\b" + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateYearRegex}", DateYearRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex); + + public static final String DateExtractor3 = "\\b({WeekDayRegex}(\\s+|\\s*,\\s*))?{MonthRegex}(\\s*[/\\.\\- ]\\s*|\\s+de\\s+){DayRegex}((\\s*[/\\.\\- ]\\s*|\\s+de\\s+){DateYearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DateExtractor4 = "\\b{MonthNumRegex}\\s*[/\\\\\\-]\\s*{DayRegex}\\s*[/\\\\\\-]\\s*{DateYearRegex}(?!\\s*[/\\\\\\-\\.]\\s*\\d+)" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DateExtractor5 = "\\b{DayRegex}\\s*[/\\\\\\-\\.]\\s*({MonthNumRegex}|{MonthRegex})\\s*[/\\\\\\-\\.]\\s*{DateYearRegex}(?!\\s*[/\\\\\\-\\.]\\s*\\d+)" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateYearRegex}", DateYearRegex); + + public static final String DateExtractor6 = "(?<=\\b(em|no|o)\\s+){MonthNumRegex}[\\-\\.]{DayRegex}{BaseDateTime.CheckDecimalRegex}\\b" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor7 = "\\b{MonthNumRegex}\\s*/\\s*{DayRegex}((\\s+|\\s*(,|de)\\s*){DateYearRegex})?{BaseDateTime.CheckDecimalRegex}\\b" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{DateYearRegex}", DateYearRegex) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor8 = "(?<=\\b(em|no|o)\\s+){DayRegex}[\\\\\\-]{MonthNumRegex}{BaseDateTime.CheckDecimalRegex}\\b" + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DayRegex}", DayRegex) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor9 = "\\b{DayRegex}\\s*/\\s*{MonthNumRegex}((\\s+|\\s*(,|de)\\s*){DateYearRegex})?{BaseDateTime.CheckDecimalRegex}\\b" + .replace("{DayRegex}", DayRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{DateYearRegex}", DateYearRegex) + .replace("{BaseDateTime.CheckDecimalRegex}", BaseDateTime.CheckDecimalRegex); + + public static final String DateExtractor10 = "\\b({YearRegex}\\s*[/\\\\\\-\\.]\\s*({MonthNumRegex}|{MonthRegex})\\s*[/\\\\\\-\\.]\\s*{DayRegex}|{MonthRegex}\\s*[/\\\\\\-\\.]\\s*{BaseDateTime.FourDigitYearRegex}\\s*[/\\\\\\-\\.]\\s*{DayRegex}|{DayRegex}\\s*[/\\\\\\-\\.]\\s*{BaseDateTime.FourDigitYearRegex}\\s*[/\\\\\\-\\.]\\s*{MonthRegex})(?!\\s*[/\\\\\\-\\.:]\\s*\\d+)" + .replace("{YearRegex}", YearRegex) + .replace("{MonthNumRegex}", MonthNumRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{DayRegex}", DayRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex); + + public static final String DateExtractor11 = "(?<=\\b(dia)\\s+){DayRegex}" + .replace("{DayRegex}", DayRegex); + + public static final String HourNumRegex = "\\b(?zero|uma|duas|tr[êe]s|[qc]uatro|cinco|seis|sete|oito|nove|dez|onze|doze)\\b"; + + public static final String MinuteNumRegex = "(?um|dois|tr[êe]s|[qc]uatro|cinco|seis|sete|oito|nove|dez|onze|doze|treze|catorze|quatorze|quinze|dez[ea]sseis|dez[ea]sete|dezoito|dez[ea]nove|vinte|trinta|[qc]uarenta|cin[qc]uenta)"; + + public static final String DeltaMinuteNumRegex = "(?um|dois|tr[êe]s|[qc]uatro|cinco|seis|sete|oito|nove|dez|onze|doze|treze|catorze|quatorze|quinze|dez[ea]sseis|dez[ea]sete|dezoito|dez[ea]nove|vinte|trinta|[qc]uarenta|cin[qc]uenta)"; + + public static final String PmRegex = "(?((pela|de|da|\\b[àa]\\b|na)\\s+(tarde|noite)))|((depois\\s+do|ap[óo]s\\s+o)\\s+(almo[çc]o|meio dia|meio-dia))"; + + public static final String AmRegex = "(?(pela|de|da|na)\\s+(manh[ãa]|madrugada))"; + + public static final String AmTimeRegex = "(?([dn]?es[st]a|(pela|de|da|na))\\s+(manh[ãa]|madrugada))"; + + public static final String PmTimeRegex = "(?(([dn]?es[st]a|\\b[àa]\\b|(pela|de|da|na))\\s+(tarde|noite)))|((depois\\s+do|ap[óo]s\\s+o)\\s+(almo[çc]o|meio dia|meio-dia))"; + + public static final String LessThanOneHour = "(?((\\s+e\\s+)?(quinze|(um\\s+|dois\\s+|tr[êes]\\s+)?quartos?)|quinze|(\\s*)(um\\s+|dois\\s+|tr[êes]\\s+)?quartos?|(\\s+e\\s+)(meia|trinta)|{BaseDateTime.DeltaMinuteRegex}(\\s+(minuto|minutos|min|mins))|{DeltaMinuteNumRegex}(\\s+(minuto|minutos|min|mins))))" + .replace("{BaseDateTime.DeltaMinuteRegex}", BaseDateTime.DeltaMinuteRegex) + .replace("{DeltaMinuteNumRegex}", DeltaMinuteNumRegex); + + public static final String TensTimeRegex = "(?dez|vinte|trinta|[qc]uarenta|cin[qc]uenta)"; + + public static final String WrittenTimeRegex = "(?({HourNumRegex}\\s*((e|menos)\\s+)?({MinuteNumRegex}|({TensTimeRegex}((\\s*e\\s+)?{MinuteNumRegex}))))|(({MinuteNumRegex}|({TensTimeRegex}((\\s*e\\s+)?{MinuteNumRegex})?))\\s*((para as|pras|antes da|antes das)\\s+)?({HourNumRegex}|{BaseDateTime.HourRegex})))" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex) + .replace("{TensTimeRegex}", TensTimeRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String TimePrefix = "(?{LessThanOneHour}(\\s+(passad[ao]s)\\s+(as)?|\\s+depois\\s+(das?|do)|\\s+pras?|\\s+(para|antes)?\\s+([àa]s?))?)" + .replace("{LessThanOneHour}", LessThanOneHour); + + public static final String TimeSuffix = "(?({LessThanOneHour}\\s+)?({AmRegex}|{PmRegex}|{OclockRegex}))" + .replace("{LessThanOneHour}", LessThanOneHour) + .replace("{AmRegex}", AmRegex) + .replace("{PmRegex}", PmRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String BasicTime = "(?{WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}:{BaseDateTime.MinuteRegex}(:{BaseDateTime.SecondRegex})?|{BaseDateTime.HourRegex})" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex); + + public static final String AtRegex = "\\b((?<=\\b([aà]s?)\\s+)({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})(\\s+horas?|\\s*h\\b)?|(?<=\\b(s(er)?[aã]o|v[aã]o\\s+ser|^[eé]h?)\\s+)({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})(\\s+horas?|\\s*h\\b))(\\s+{OclockRegex})?\\b" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String ConnectNumRegex = "({BaseDateTime.HourRegex}(?[0-5][0-9])\\s*{DescRegex})" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex1 = "(\\b{TimePrefix}\\s+)?({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})\\s*({DescRegex})" + .replace("{TimePrefix}", TimePrefix) + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex2 = "(\\b{TimePrefix}\\s+)?(t)?{BaseDateTime.HourRegex}(\\s*)?:(\\s*)?{BaseDateTime.MinuteRegex}((\\s*)?:(\\s*)?{BaseDateTime.SecondRegex})?((\\s*{DescRegex})|\\b)" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex3 = "(\\b{TimePrefix}\\s+)?{BaseDateTime.HourRegex}\\.{BaseDateTime.MinuteRegex}(\\s*{DescRegex})" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex4 = "\\b(({DescRegex}?)|({BasicTime}?)({DescRegex}?))({TimePrefix}\\s*)({HourNumRegex}|{BaseDateTime.HourRegex})?(\\s+{TensTimeRegex}(\\s+e\\s+)?{MinuteNumRegex}?)?({OclockRegex})?\\b" + .replace("{DescRegex}", DescRegex) + .replace("{BasicTime}", BasicTime) + .replace("{TimePrefix}", TimePrefix) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TensTimeRegex}", TensTimeRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String TimeRegex5 = "\\b({TimePrefix}|{BasicTime}{TimePrefix})\\s+(\\s*{DescRegex})?{BasicTime}?\\s*{TimeSuffix}\\b" + .replace("{TimePrefix}", TimePrefix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex6 = "({BasicTime}(\\s*{DescRegex})?\\s+{TimeSuffix}\\b)" + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex7 = "\\b{TimeSuffix}\\s+[àa]s?\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffix}", TimeSuffix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex8 = "\\b{TimeSuffix}\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffix}", TimeSuffix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex9 = "\\b(?{HourNumRegex}\\s+({TensTimeRegex}\\s*)(e\\s+)?{MinuteNumRegex}?)\\b" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{TensTimeRegex}", TensTimeRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex); + + public static final String TimeRegex10 = "(\\b([àa]|ao?)|na|de|da|pela)\\s+(madrugada|manh[ãa]|meio\\s*dia|meia\\s*noite|tarde|noite)"; + + public static final String TimeRegex11 = "\\b({WrittenTimeRegex})(\\s+{DescRegex})?\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex12 = "(\\b{TimePrefix}\\s+)?{BaseDateTime.HourRegex}(\\s*h\\s*){BaseDateTime.MinuteRegex}(\\s*{DescRegex})?" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String PrepositionRegex = "(?([àa]s?|em|por|pel[ao]|n[ao]|de|d[ao]?)?$)"; + + public static final String NowRegex = "\\b(?((logo|exatamente)\\s+)?agora(\\s+mesmo)?|neste\\s+momento|(assim\\s+que|t[ãa]o\\s+cedo\\s+quanto)\\s+(poss[ií]vel|possas?|possamos)|o\\s+mais\\s+(cedo|r[aá]pido)\\s+poss[íi]vel|recentemente|previamente)\\b"; + + public static final String SuffixRegex = "^\\s*((e|a|em|por|pel[ao]|n[ao]|de)\\s+)?(manh[ãa]|madrugada|meio\\s*dia|tarde|noite)\\b"; + + public static final String TimeOfDayRegex = "\\b(?manh[ãa]|madrugada|tarde|noite|((depois\\s+do|ap[óo]s\\s+o)\\s+(almo[çc]o|meio dia|meio-dia)))\\b"; + + public static final String SpecificTimeOfDayRegex = "\\b(((((a)?\\s+|[nd]?es[st]a|seguinte|pr[oó]xim[oa]|[uú]ltim[oa])\\s+)?{TimeOfDayRegex}))\\b" + .replace("{TimeOfDayRegex}", TimeOfDayRegex); + + public static final String TimeOfTodayAfterRegex = "^\\s*(,\\s*)?([àa]|em|por|pel[ao]|de|no|na?\\s+)?{SpecificTimeOfDayRegex}" + .replace("{SpecificTimeOfDayRegex}", SpecificTimeOfDayRegex); + + public static final String TimeOfTodayBeforeRegex = "({SpecificTimeOfDayRegex}(\\s*,)?(\\s+([àa]s|para))?\\s*)" + .replace("{SpecificTimeOfDayRegex}", SpecificTimeOfDayRegex); + + public static final String SimpleTimeOfTodayAfterRegex = "({HourNumRegex}|{BaseDateTime.HourRegex})\\s*(,\\s*)?{SpecificTimeOfDayRegex}" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{SpecificTimeOfDayRegex}", SpecificTimeOfDayRegex); + + public static final String SimpleTimeOfTodayBeforeRegex = "({SpecificTimeOfDayRegex}(\\s*,)?(\\s+([àa]s|((cerca|perto|ao\\s+redor|por\\s+volta)\\s+(de|das))))?\\s*({HourNumRegex}|{BaseDateTime.HourRegex}))" + .replace("{SpecificTimeOfDayRegex}", SpecificTimeOfDayRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex); + + public static final String SpecificEndOfRegex = "((no|ao)\\s+)?(fi(m|nal)|t[ée]rmin(o|ar))(\\s+d?o(\\s+dia)?(\\s+de)?)?\\s*$"; + + public static final String UnspecificEndOfRegex = "^[.]"; + + public static final String UnspecificEndOfRangeRegex = "^[.]"; + + public static final String UnitRegex = "(?anos?|meses|m[êe]s|semanas?|dias?|horas?|hrs?|hs?|minutos?|mins?|segundos?|segs?)\\b"; + + public static final String ConnectorRegex = "^(,|t|para [ao]|para as|pras|(cerca|perto|ao\\s+redor|por\\s+volta)\\s+(de|das)|quase)$"; + + public static final String TimeHourNumRegex = "(?vinte( e (um|dois|tr[êe]s|quatro))?|zero|uma?|dois|duas|tr[êe]s|quatro|cinco|seis|sete|oito|nove|dez|onze|doze|treze|quatorze|catorze|quinze|dez([ea]sseis|[ea]ssete|oito|[ea]nove))"; + + public static final String PureNumFromTo = "(((desde|de|da|das)\\s+(a(s)?\\s+)?)?({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?\\s*{TillRegex}(?{DescRegex}))?\\s*{TillRegex})\\s*({BaseDateTime.HourRegex}|{TimeHourNumRegex})\\s*(?{PmRegex}|{AmRegex}|{DescRegex})?" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{TillRegex}", TillRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex); + + public static final String PureNumBetweenAnd = "(entre\\s+((a|as)?\\s+)?)({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?\\s*e\\s*(a(s)?\\s+)?({BaseDateTime.HourRegex}|{TimeHourNumRegex})\\s*(?{PmRegex}|{AmRegex}|{DescRegex})?" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex); + + public static final String SpecificTimeFromTo = "^[.]"; + + public static final String SpecificTimeBetweenAnd = "^[.]"; + + public static final String TimeUnitRegex = "(?horas?|h|minutos?|mins?|segundos?|se[cg]s?)\\b"; + + public static final String TimeFollowedUnit = "^\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String TimeNumberCombinedWithUnit = "\\b(?\\d+(\\,\\d*)?)\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String DateTimePeriodNumberCombinedWithUnit = "\\b(?\\d+(\\.\\d*)?)\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String PeriodTimeOfDayWithDateRegex = "\\b((e|[àa]|em|na|no|ao|pel[ao]|de)\\s+)?(?manh[ãa]|madrugada|(passado\\s+(o\\s+)?)?meio\\s+dia|tarde|noite)\\b"; + + public static final String RelativeTimeUnitRegex = "({PastRegex}|{FutureRegex})\\s+{UnitRegex}|{UnitRegex}\\s+({PastRegex}|{FutureRegex})" + .replace("{PastRegex}", PastRegex) + .replace("{FutureRegex}", FutureRegex) + .replace("{UnitRegex}", UnitRegex); + + public static final String SuffixAndRegex = "(?\\s*(e)\\s+(?meia|(um\\s+)?quarto))"; + + public static final String FollowedUnit = "^\\s*{UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String LessThanRegex = "^[.]"; + + public static final String MoreThanRegex = "^[.]"; + + public static final String DurationNumberCombinedWithUnit = "\\b(?\\d+(\\,\\d*)?){UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String AnUnitRegex = "\\b(um(a)?)\\s+{UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String DuringRegex = "^[.]"; + + public static final String AllRegex = "\\b(?tod[oa]?\\s+(o|a)\\s+(?ano|m[êe]s|semana|dia))\\b"; + + public static final String HalfRegex = "\\b(?mei[oa]\\s+(?ano|m[êe]s|semana|dia|hora))\\b"; + + public static final String ConjunctionRegex = "^[.]"; + + public static final String InexactNumberRegex = "\\b(poucos|pouco|algum|alguns|v[áa]rios)\\b"; + + public static final String InexactNumberUnitRegex = "\\b(poucos|pouco|algum|alguns|v[áa]rios)\\s+{UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String HolidayRegex1 = "\\b(?sexta-feira santa|sexta-feira da paix[ãa]o|quarta-feira de cinzas|carnaval|dia dos? presidentes?|ano novo chin[eê]s|ano novo|v[ée]spera de ano novo|natal|v[ée]spera de natal|dia de a[cç][ãa]o de gra[çc]as|a[cç][ãa]o de gra[çc]as|yuandan|halloween|dia das bruxas|p[áa]scoa)(\\s+(d[eo]?\\s+)?({YearRegex}|(?(pr[oó]xim[oa]?|[nd]?es[st][ea]|[uú]ltim[oa]?|em))\\s+ano))?\\b" + .replace("{YearRegex}", YearRegex); + + public static final String HolidayRegex2 = "\\b(?(dia\\s+(d[eoa]s?\\s+)?)?(martin luther king|todos os santos|s[ãa]o (patr[íi]cio|francisco|jorge|jo[ãa]o)|independ[êe]ncia))(\\s+(d[eo]?\\s+)?({YearRegex}|(?(pr[oó]xim[oa]?|[nd]?es[st][ea]|[uú]ltim[oa]?|em))\\s+ano))?\\b" + .replace("{YearRegex}", YearRegex); + + public static final String HolidayRegex3 = "\\b(?(dia\\s+d[eoa]s?\\s+)(trabalh(o|ador(es)?)|m[ãa]es?|pais?|mulher(es)?|crian[çc]as?|marmota|professor(es)?))(\\s+(d[eo]?\\s+)?({YearRegex}|(?(pr[oó]xim[oa]?|[nd]?es[st][ea]|[uú]ltim[oa]?|em))\\s+ano))?\\b" + .replace("{YearRegex}", YearRegex); + + public static final String BeforeRegex = "(antes(\\s+(de|dos?|das?)?)?)"; + + public static final String AfterRegex = "((depois|ap[óo]s)(\\s*(de|d?os?|d?as?)?)?)"; + + public static final String SinceRegex = "(desde(\\s+(as?|o))?)"; + + public static final String AroundRegex = "(?:\\b(?:cerca|perto|ao\\s+redor|por\\s+volta)\\s*?\\b)(\\s+(de|das))?"; + + public static final String PeriodicRegex = "\\b(?di[áa]ri[ao]|diariamente|mensalmente|semanalmente|quinzenalmente|anualmente)\\b"; + + public static final String EachExpression = "cada|tod[oa]s?\\s*([oa]s)?"; + + public static final String EachUnitRegex = "(?({EachExpression})\\s*{UnitRegex})" + .replace("{EachExpression}", EachExpression) + .replace("{UnitRegex}", UnitRegex); + + public static final String EachPrefixRegex = "(?({EachExpression})\\s*$)" + .replace("{EachExpression}", EachExpression); + + public static final String EachDayRegex = "\\s*({EachExpression})\\s*dias\\s*\\b" + .replace("{EachExpression}", EachExpression); + + public static final String BeforeEachDayRegex = "({EachExpression})\\s*dias(\\s+(as|ao))?\\s*\\b" + .replace("{EachExpression}", EachExpression); + + public static final String SetEachRegex = "(?({EachExpression})\\s*)" + .replace("{EachExpression}", EachExpression); + + public static final String LaterEarlyPeriodRegex = "^[.]"; + + public static final String WeekWithWeekDayRangeRegex = "^[.]"; + + public static final String GeneralEndingRegex = "^[.]"; + + public static final String MiddlePauseRegex = "^[.]"; + + public static final String PrefixArticleRegex = "^[\\.]"; + + public static final String OrRegex = "^[.]"; + + public static final String YearPlusNumberRegex = "^[.]"; + + public static final String NumberAsTimeRegex = "^[.]"; + + public static final String TimeBeforeAfterRegex = "^[.]"; + + public static final String DateNumberConnectorRegex = "^[.]"; + + public static final String ComplexDatePeriodRegex = "^[.]"; + + public static final String AgoRegex = "\\b(antes|atr[áa]s|no passado)\\b"; + + public static final String LaterRegex = "\\b(depois d[eoa]s?|ap[óo]s (as)?|desde( (as|o))?|no futuro|mais tarde)\\b"; + + public static final String Tomorrow = "amanh[ãa]"; + + public static final ImmutableMap UnitMap = ImmutableMap.builder() + .put("anos", "Y") + .put("ano", "Y") + .put("meses", "MON") + .put("mes", "MON") + .put("mês", "MON") + .put("semanas", "W") + .put("semana", "W") + .put("dias", "D") + .put("dia", "D") + .put("horas", "H") + .put("hora", "H") + .put("hrs", "H") + .put("hr", "H") + .put("h", "H") + .put("minutos", "M") + .put("minuto", "M") + .put("mins", "M") + .put("min", "M") + .put("segundos", "S") + .put("segundo", "S") + .put("segs", "S") + .put("seg", "S") + .build(); + + public static final ImmutableMap UnitValueMap = ImmutableMap.builder() + .put("anos", 31536000L) + .put("ano", 31536000L) + .put("meses", 2592000L) + .put("mes", 2592000L) + .put("mês", 2592000L) + .put("semanas", 604800L) + .put("semana", 604800L) + .put("dias", 86400L) + .put("dia", 86400L) + .put("horas", 3600L) + .put("hora", 3600L) + .put("hrs", 3600L) + .put("hr", 3600L) + .put("h", 3600L) + .put("minutos", 60L) + .put("minuto", 60L) + .put("mins", 60L) + .put("min", 60L) + .put("segundos", 1L) + .put("segundo", 1L) + .put("segs", 1L) + .put("seg", 1L) + .build(); + + public static final ImmutableMap SpecialYearPrefixesMap = ImmutableMap.builder() + .put("", "") + .build(); + + public static final ImmutableMap SeasonMap = ImmutableMap.builder() + .put("primavera", "SP") + .put("verao", "SU") + .put("verão", "SU") + .put("outono", "FA") + .put("inverno", "WI") + .build(); + + public static final ImmutableMap SeasonValueMap = ImmutableMap.builder() + .put("SP", 3) + .put("SU", 6) + .put("FA", 9) + .put("WI", 12) + .build(); + + public static final ImmutableMap CardinalMap = ImmutableMap.builder() + .put("primeiro", 1) + .put("primeira", 1) + .put("1o", 1) + .put("1a", 1) + .put("segundo", 2) + .put("segunda", 2) + .put("2o", 2) + .put("2a", 2) + .put("terceiro", 3) + .put("terceira", 3) + .put("3o", 3) + .put("3a", 3) + .put("cuarto", 4) + .put("quarto", 4) + .put("cuarta", 4) + .put("quarta", 4) + .put("4o", 4) + .put("4a", 4) + .put("quinto", 5) + .put("quinta", 5) + .put("5o", 5) + .put("5a", 5) + .build(); + + public static final ImmutableMap DayOfWeek = ImmutableMap.builder() + .put("segunda-feira", 1) + .put("segundas-feiras", 1) + .put("segunda feira", 1) + .put("segundas feiras", 1) + .put("segunda", 1) + .put("segundas", 1) + .put("terça-feira", 2) + .put("terças-feiras", 2) + .put("terça feira", 2) + .put("terças feiras", 2) + .put("terça", 2) + .put("terças", 2) + .put("terca-feira", 2) + .put("tercas-feiras", 2) + .put("terca feira", 2) + .put("tercas feiras", 2) + .put("terca", 2) + .put("tercas", 2) + .put("quarta-feira", 3) + .put("quartas-feiras", 3) + .put("quarta feira", 3) + .put("quartas feiras", 3) + .put("quarta", 3) + .put("quartas", 3) + .put("quinta-feira", 4) + .put("quintas-feiras", 4) + .put("quinta feira", 4) + .put("quintas feiras", 4) + .put("quinta", 4) + .put("quintas", 4) + .put("sexta-feira", 5) + .put("sextas-feiras", 5) + .put("sexta feira", 5) + .put("sextas feiras", 5) + .put("sexta", 5) + .put("sextas", 5) + .put("sabado", 6) + .put("sabados", 6) + .put("sábado", 6) + .put("sábados", 6) + .put("domingo", 0) + .put("domingos", 0) + .put("seg", 1) + .put("seg.", 1) + .put("2a", 1) + .put("ter", 2) + .put("ter.", 2) + .put("3a", 2) + .put("qua", 3) + .put("qua.", 3) + .put("4a", 3) + .put("qui", 4) + .put("qui.", 4) + .put("5a", 4) + .put("sex", 5) + .put("sex.", 5) + .put("6a", 5) + .put("sab", 6) + .put("sab.", 6) + .put("dom", 0) + .put("dom.", 0) + .build(); + + public static final ImmutableMap MonthOfYear = ImmutableMap.builder() + .put("janeiro", 1) + .put("fevereiro", 2) + .put("março", 3) + .put("marco", 3) + .put("abril", 4) + .put("maio", 5) + .put("junho", 6) + .put("julho", 7) + .put("agosto", 8) + .put("septembro", 9) + .put("setembro", 9) + .put("outubro", 10) + .put("novembro", 11) + .put("dezembro", 12) + .put("jan", 1) + .put("fev", 2) + .put("mar", 3) + .put("abr", 4) + .put("mai", 5) + .put("jun", 6) + .put("jul", 7) + .put("ago", 8) + .put("sept", 9) + .put("set", 9) + .put("out", 10) + .put("nov", 11) + .put("dez", 12) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .build(); + + public static final ImmutableMap Numbers = ImmutableMap.builder() + .put("zero", 0) + .put("um", 1) + .put("uma", 1) + .put("dois", 2) + .put("tres", 3) + .put("três", 3) + .put("quatro", 4) + .put("cinco", 5) + .put("seis", 6) + .put("sete", 7) + .put("oito", 8) + .put("nove", 9) + .put("dez", 10) + .put("onze", 11) + .put("doze", 12) + .put("dezena", 12) + .put("dezenas", 12) + .put("treze", 13) + .put("catorze", 14) + .put("quatorze", 14) + .put("quinze", 15) + .put("dezesseis", 16) + .put("dezasseis", 16) + .put("dezessete", 17) + .put("dezassete", 17) + .put("dezoito", 18) + .put("dezenove", 19) + .put("dezanove", 19) + .put("vinte", 20) + .put("vinte e um", 21) + .put("vinte e uma", 21) + .put("vinte e dois", 22) + .put("vinte e duas", 22) + .put("vinte e tres", 23) + .put("vinte e três", 23) + .put("vinte e quatro", 24) + .put("vinte e cinco", 25) + .put("vinte e seis", 26) + .put("vinte e sete", 27) + .put("vinte e oito", 28) + .put("vinte e nove", 29) + .put("trinta", 30) + .build(); + + public static final ImmutableMap HolidayNames = ImmutableMap.builder() + .put("pai", new String[]{"diadopai", "diadospais"}) + .put("mae", new String[]{"diadamae", "diadasmaes"}) + .put("acaodegracas", new String[]{"diadegracas", "diadeacaodegracas", "acaodegracas"}) + .put("trabalho", new String[]{"diadotrabalho", "diadotrabalhador", "diadostrabalhadores"}) + .put("pascoa", new String[]{"diadepascoa", "pascoa"}) + .put("natal", new String[]{"natal", "diadenatal"}) + .put("vesperadenatal", new String[]{"vesperadenatal"}) + .put("anonovo", new String[]{"anonovo", "diadeanonovo", "diadoanonovo"}) + .put("vesperadeanonovo", new String[]{"vesperadeanonovo", "vesperadoanonovo"}) + .put("yuandan", new String[]{"yuandan"}) + .put("todosossantos", new String[]{"todosossantos"}) + .put("professor", new String[]{"diadoprofessor", "diadosprofessores"}) + .put("crianca", new String[]{"diadacrianca", "diadascriancas"}) + .put("mulher", new String[]{"diadamulher"}) + .build(); + + public static final ImmutableMap VariableHolidaysTimexDictionary = ImmutableMap.builder() + .put("pai", "-06-WXX-7-3") + .put("mae", "-05-WXX-7-2") + .put("acaodegracas", "-11-WXX-4-4") + .put("memoria", "-03-WXX-2-4") + .build(); + + public static final ImmutableMap DoubleNumbers = ImmutableMap.builder() + .put("metade", 0.5D) + .put("quarto", 0.25D) + .build(); + + public static final String DateTokenPrefix = "em "; + + public static final String TimeTokenPrefix = "as "; + + public static final String TokenBeforeDate = "o "; + + public static final String TokenBeforeTime = "as "; + + public static final String UpcomingPrefixRegex = ".^"; + + public static final String NextPrefixRegex = "(pr[oó]xim[oa]|seguinte|{UpcomingPrefixRegex})\\b" + .replace("{UpcomingPrefixRegex}", UpcomingPrefixRegex); + + public static final String PastPrefixRegex = ".^"; + + public static final String PreviousPrefixRegex = "([uú]ltim[oa]|{PastPrefixRegex})\\b" + .replace("{PastPrefixRegex}", PastPrefixRegex); + + public static final String ThisPrefixRegex = "([nd]?es[st][ea])\\b"; + + public static final String RelativeDayRegex = "^[\\.]"; + + public static final String RestOfDateRegex = "^[\\.]"; + + public static final String RelativeDurationUnitRegex = "^[\\.]"; + + public static final String ReferenceDatePeriodRegex = "^[.]"; + + public static final String FromToRegex = "\\b(from).+(to)\\b.+"; + + public static final String SingleAmbiguousMonthRegex = "^(the\\s+)?(may|march)$"; + + public static final String UnspecificDatePeriodRegex = "^[.]"; + + public static final String PrepositionSuffixRegex = "\\b(on|in|at|around|from|to)$"; + + public static final String RestOfDateTimeRegex = "^[\\.]"; + + public static final String SetWeekDayRegex = "^[\\.]"; + + public static final String NightRegex = "\\b(meia noite|noite|de noite)\\b"; + + public static final String CommonDatePrefixRegex = "\\b(dia)\\s+$"; + + public static final String DurationUnitRegex = "^[\\.]"; + + public static final String DurationConnectorRegex = "^[.]"; + + public static final String CenturyRegex = "^[.]"; + + public static final String DecadeRegex = "^[.]"; + + public static final String DecadeWithCenturyRegex = "^[.]"; + + public static final String RelativeDecadeRegex = "^[.]"; + + public static final String YearSuffix = "((,|\\sde)?\\s*({YearRegex}|{FullTextYearRegex}))" + .replace("{YearRegex}", YearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String SuffixAfterRegex = "^[.]"; + + public static final String YearPeriodRegex = "^[.]"; + + public static final String FutureSuffixRegex = "^[.]"; + + public static final ImmutableMap WrittenDecades = ImmutableMap.builder() + .put("", 0) + .build(); + + public static final ImmutableMap SpecialDecadeCases = ImmutableMap.builder() + .put("", 0) + .build(); + + public static final String DefaultLanguageFallback = "DMY"; + + public static final List DurationDateRestrictions = Arrays.asList(); + + public static final ImmutableMap AmbiguityFiltersDict = ImmutableMap.builder() + .put("^(abr|ago|dez|fev|jan|ju[ln]|mar|maio?|nov|out|sep?t)$", "([$%£&!?@#])(abr|ago|dez|fev|jan|ju[ln]|mar|maio?|nov|out|sep?t)|(abr|ago|dez|fev|jan|ju[ln]|mar|maio?|nov|out|sep?t)([$%£&@#])") + .build(); + + public static final List EarlyMorningTermList = Arrays.asList("madrugada"); + + public static final List MorningTermList = Arrays.asList("manha", "manhã"); + + public static final List AfternoonTermList = Arrays.asList("passado o meio dia", "depois do meio dia"); + + public static final List EveningTermList = Arrays.asList("tarde"); + + public static final List NightTermList = Arrays.asList("noite"); + + public static final List SameDayTerms = Arrays.asList("hoje", "este dia", "esse dia", "o dia"); + + public static final List PlusOneDayTerms = Arrays.asList("amanha", "de amanha", "dia seguinte", "o dia de amanha", "proximo dia"); + + public static final List MinusOneDayTerms = Arrays.asList("ontem", "ultimo dia"); + + public static final List PlusTwoDayTerms = Arrays.asList("depois de amanha", "dia depois de amanha"); + + public static final List MinusTwoDayTerms = Arrays.asList("anteontem", "dia antes de ontem"); + + public static final List MonthTerms = Arrays.asList("mes", "meses"); + + public static final List MonthToDateTerms = Arrays.asList("mes ate agora", "mes ate hoje", "mes ate a data"); + + public static final List WeekendTerms = Arrays.asList("fim de semana"); + + public static final List WeekTerms = Arrays.asList("semana"); + + public static final List YearTerms = Arrays.asList("ano", "anos"); + + public static final List YearToDateTerms = Arrays.asList("ano ate agora", "ano ate hoje", "ano ate a data", "anos ate agora", "anos ate hoje", "anos ate a data"); + + public static final ImmutableMap SpecialCharactersEquivalent = ImmutableMap.builder() + .put('á', 'a') + .put('é', 'e') + .put('í', 'i') + .put('ó', 'o') + .put('ú', 'u') + .put('ê', 'e') + .put('ô', 'o') + .put('ü', 'u') + .put('ã', 'a') + .put('õ', 'o') + .put('ç', 'c') + .build(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/SpanishDateTime.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/SpanishDateTime.java new file mode 100644 index 000000000..d8330a668 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/resources/SpanishDateTime.java @@ -0,0 +1,1228 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +package com.microsoft.recognizers.text.datetime.resources; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +public class SpanishDateTime { + + public static final String LangMarker = "Spa"; + + public static final Boolean CheckBothBeforeAfter = false; + + public static final String TillRegex = "(?\\b(hasta|hacia|al?)\\b(\\s+(el|la(s)?)\\b)?|{BaseDateTime.RangeConnectorSymbolRegex})" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String StrictTillRegex = "(?\\b(hasta|hacia|al?)(\\s+(el|la(s)?))?\\b|{BaseDateTime.RangeConnectorSymbolRegex}(?!\\s*[qt][1-4](?!(\\s+de|\\s*,\\s*))))" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String RangeConnectorRegex = "(?\\b(y\\s*(el|(la(s)?)?))\\b|{BaseDateTime.RangeConnectorSymbolRegex})" + .replace("{BaseDateTime.RangeConnectorSymbolRegex}", BaseDateTime.RangeConnectorSymbolRegex); + + public static final String WrittenDayRegex = "(?uno|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez|once|doce|trece|catorce|quince|dieciséis|diecisiete|dieciocho|diecinueve|veinte|veintiuno|veintidós|veintitrés|veinticuatro|veinticinco|veintiséis|veintisiete|veintiocho|veintinueve|treinta(\\s+y\\s+uno)?)"; + + public static final String DayRegex = "\\b(?01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|1|20|21|22|23|24|25|26|27|28|29|2|30|31|3|4|5|6|7|8|9)(?:\\.[º°])?(?=\\b|t)"; + + public static final String MonthNumRegex = "(?1[0-2]|(0)?[1-9])\\b"; + + public static final String OclockRegex = "(?en\\s+punto)"; + + public static final String AmDescRegex = "({BaseDateTime.BaseAmDescRegex})" + .replace("{BaseDateTime.BaseAmDescRegex}", BaseDateTime.BaseAmDescRegex); + + public static final String PmDescRegex = "({BaseDateTime.BasePmDescRegex})" + .replace("{BaseDateTime.BasePmDescRegex}", BaseDateTime.BasePmDescRegex); + + public static final String AmPmDescRegex = "({BaseDateTime.BaseAmPmDescRegex})" + .replace("{BaseDateTime.BaseAmPmDescRegex}", BaseDateTime.BaseAmPmDescRegex); + + public static final String DescRegex = "(?({AmDescRegex}|{PmDescRegex}))" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex); + + public static final String OfPrepositionRegex = "(\\bd(o|al?|el?)\\b)"; + + public static final String AfterNextSuffixRegex = "\\b(despu[eé]s\\s+de\\s+la\\s+pr[oó]xima)\\b"; + + public static final String NextSuffixRegex = "\\b(que\\s+viene|pr[oó]xim[oa]|siguiente)\\b"; + + public static final String PreviousSuffixRegex = "\\b(pasad[ao]|anterior(?!\\s+(al?|del?)\\b))\\b"; + + public static final String RelativeSuffixRegex = "({AfterNextSuffixRegex}|{NextSuffixRegex}|{PreviousSuffixRegex})" + .replace("{AfterNextSuffixRegex}", AfterNextSuffixRegex) + .replace("{NextSuffixRegex}", NextSuffixRegex) + .replace("{PreviousSuffixRegex}", PreviousSuffixRegex); + + public static final String RangePrefixRegex = "((de(l|sde)?|entre)(\\s+la(s)?)?)"; + + public static final String TwoDigitYearRegex = "\\b(?([0-9]\\d))(?!(\\s*((\\:\\d)|{AmDescRegex}|{PmDescRegex}|\\.\\d))|\\.?[º°ª])\\b" + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex); + + public static final String RelativeRegex = "(?est[ae]|pr[oó]xim[oa]|siguiente|(([uú]ltim|pasad)(o|as|os)))\\b"; + + public static final String StrictRelativeRegex = "(?est[ae]|pr[oó]xim[oa]|siguiente|(([uú]ltim|pasad)(o|as|os)))\\b"; + + public static final String WrittenOneToNineRegex = "(un[ao]?|dos|tres|cuatro|cinco|seis|siete|ocho|nueve)"; + + public static final String WrittenOneHundredToNineHundredRegex = "(doscient[oa]s|trescient[oa]s|cuatrocient[ao]s|quinient[ao]s|seiscient[ao]s|setecient[ao]s|ochocient[ao]s|novecient[ao]s|cien(to)?)"; + + public static final String WrittenOneToNinetyNineRegex = "(((treinta|cuarenta|cincuenta|sesenta|setenta|ochenta|noventa)(\\s+y\\s+{WrittenOneToNineRegex})?)|diez|once|doce|trece|catorce|quince|dieciséis|dieciseis|diecisiete|dieciocho|diecinueve|veinte|veintiuno|veintiún|veintiun|veintiuna|veintidós|veintidos|veintitrés|veintitres|veinticuatro|veinticinco|veintiséis|veintisiete|veintiocho|veintinueve|un[ao]?|dos|tres|cuatro|cinco|seis|siete|ocho|nueve)" + .replace("{WrittenOneToNineRegex}", WrittenOneToNineRegex); + + public static final String FullTextYearRegex = "\\b(?((dos\\s+)?mil)(\\s+{WrittenOneHundredToNineHundredRegex})?(\\s+{WrittenOneToNinetyNineRegex})?)" + .replace("{WrittenOneToNinetyNineRegex}", WrittenOneToNinetyNineRegex) + .replace("{WrittenOneHundredToNineHundredRegex}", WrittenOneHundredToNineHundredRegex); + + public static final String YearRegex = "({BaseDateTime.FourDigitYearRegex}|{FullTextYearRegex})" + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String RelativeMonthRegex = "(?(de\\s+)?((este|pr[oó]ximo|([uú]ltim(o|as|os)))\\s+mes)|(del\\s+)?(mes\\s+((que\\s+viene)|pasado)))\\b"; + + public static final String MonthRegex = "\\b(?abr(\\.|(il)?\\b)|ago(\\.|(sto)?\\b)|dic(\\.|(iembre)?\\b)|feb(\\.|(rero)?\\b)|ene(\\.|(ro)?\\b)|ju[ln](\\.|(io)?\\b)|mar(\\.|(zo)?\\b)|may(\\.|(o)?\\b)|nov(\\.|(iembre)?\\b)|oct(\\.|(ubre)?\\b)|sep?t(\\.|(iembre)?\\b)|sep(\\.|\\b))"; + + public static final String MonthSuffixRegex = "(?((del?|la|el)\\s+)?({RelativeMonthRegex}|{MonthRegex}))" + .replace("{RelativeMonthRegex}", RelativeMonthRegex) + .replace("{MonthRegex}", MonthRegex); + + public static final String DateUnitRegex = "(?años?|mes(es)?|semanas?|d[ií]as?(?\\s+(h[aá]biles|laborales))?)\\b"; + + public static final String PastRegex = "(?\\b(pasad(a|o)(s)?|[uú]ltim[oa](s)?|anterior(es)?|previo(s)?)\\b)"; + + public static final String FutureRegex = "\\b(siguiente(s)?|pr[oó]xim[oa](s)?|dentro\\s+de|en)\\b"; + + public static final String SimpleCasesRegex = "\\b((desde(\\s+el)?|entre|del?)\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*)((en|del?)\\s+)?{YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthFrontSimpleCasesRegex = "\\b{MonthSuffixRegex}\\s+((desde(\\s+el)?|entre|del)\\s+)?({DayRegex})\\s*{TillRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*)((en|del?)\\s+)?{YearRegex})?\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{DayRegex}", DayRegex) + .replace("{TillRegex}", TillRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthFrontBetweenRegex = "\\b{MonthSuffixRegex}\\s+((entre(\\s+el)?)\\s+)({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})((\\s+|\\s*,\\s*)((en|del?)\\s+)?{YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String DayBetweenRegex = "\\b((entre(\\s+el)?)\\s+)({DayRegex})\\s*{RangeConnectorRegex}\\s*({DayRegex})\\s+{MonthSuffixRegex}((\\s+|\\s*,\\s*)((en|del?)\\s+)?{YearRegex})?\\b" + .replace("{DayRegex}", DayRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{YearRegex}", YearRegex); + + public static final String SpecialYearPrefixes = "((del\\s+)?calend[aá]rio|(?fiscal|escolar))"; + + public static final String OneWordPeriodRegex = "\\b(((((la|el)\\s+)?mes\\s+(({OfPrepositionRegex})\\s+)?)|((pr[oó]xim[oa]?|est[ea]|[uú]ltim[oa]?)\\s+))?({MonthRegex})|(((la|el)\\s+)?((({RelativeRegex}\\s+)({DateUnitRegex}|(fin\\s+de\\s+)?semana|finde)(\\s+{RelativeSuffixRegex})?)|{DateUnitRegex}(\\s+{RelativeSuffixRegex}))|va\\s+de\\s+{DateUnitRegex}|((año|mes)|((el\\s+)?fin\\s+de\\s+)?semana|(el\\s+)?finde))\\b)" + .replace("{MonthRegex}", MonthRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{OfPrepositionRegex}", OfPrepositionRegex) + .replace("{RelativeSuffixRegex}", RelativeSuffixRegex) + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String MonthWithYearRegex = "\\b(((pr[oó]xim[oa](s)?|est?[ae]|[uú]ltim[oa]?)\\s+)?({MonthRegex})(\\s+|(\\s*[,-]\\s*))((de(l|\\s+la)?|en)\\s+)?({YearRegex}|(?pr[oó]ximo(s)?|[uú]ltimo?|este)\\s+año))\\b" + .replace("{MonthRegex}", MonthRegex) + .replace("{YearRegex}", YearRegex); + + public static final String MonthNumWithYearRegex = "\\b(({YearRegex}(\\s*?)[/\\-\\.~](\\s*?){MonthNumRegex})|({MonthNumRegex}(\\s*?)[/\\-\\.~](\\s*?){YearRegex}))\\b" + .replace("{YearRegex}", YearRegex) + .replace("{MonthNumRegex}", MonthNumRegex); + + public static final String WeekOfMonthRegex = "(?(la\\s+)?(?primera?|1ra|segunda|2da|tercera?|3ra|cuarta|4ta|quinta|5ta|([12345](\\.)?ª)|[uú]ltima)\\s+semana\\s+{MonthSuffixRegex}((\\s+de)?\\s+({BaseDateTime.FourDigitYearRegex}|{RelativeRegex}\\s+año))?)\\b" + .replace("{MonthSuffixRegex}", MonthSuffixRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String WeekOfYearRegex = "(?(la\\s+)?(?primera?|1ra|segunda|2da|tercera?|3ra|cuarta|4ta|quinta|5ta|[uú]ltima?|([12345]ª))\\s+semana(\\s+(del?|en))?\\s+({YearRegex}|(?pr[oó]ximo|[uú]ltimo|este)\\s+año))" + .replace("{YearRegex}", YearRegex); + + public static final String FollowedDateUnit = "^\\s*{DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String NumberCombinedWithDateUnit = "\\b(?\\d+(\\.\\d*)?){DateUnitRegex}" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String QuarterTermRegex = "\\b((?primer|1er|segundo|2do|tercer|3ro|4to|([1234](\\.)?º))\\s+(trimestre|cuarto)|[tq](?[1-4]))\\b"; + + public static final String RelativeQuarterTermRegex = "\\b((?{StrictRelativeRegex})\\s+(trimestre|cuarto)|(trimestre|cuarto)\\s+(?(actual|pr[oó]ximo|siguiente|pasado|anterior)))\\b" + .replace("{StrictRelativeRegex}", StrictRelativeRegex); + + public static final String QuarterRegex = "(el\\s+)?{QuarterTermRegex}((\\s+(del?\\s+)?|\\s*[,-]\\s*)({YearRegex}|(?pr[oó]ximo(s)?|[uú]ltimo?|este)\\s+a[ñn]o|a[ñn]o(\\s+{RelativeSuffixRegex}))|\\s+del\\s+a[ñn]o)?|{RelativeQuarterTermRegex}" + .replace("{YearRegex}", YearRegex) + .replace("{QuarterTermRegex}", QuarterTermRegex) + .replace("{RelativeRegex}", RelativeRegex) + .replace("{RelativeSuffixRegex}", RelativeSuffixRegex) + .replace("{RelativeQuarterTermRegex}", RelativeQuarterTermRegex); + + public static final String QuarterRegexYearFront = "({YearRegex}|(?pr[oó]ximo(s)?|[uú]ltimo?|este)\\s+a[ñn]o)(?:\\s*-\\s*|\\s+(el\\s+)?)?{QuarterTermRegex}" + .replace("{YearRegex}", YearRegex) + .replace("{QuarterTermRegex}", QuarterTermRegex); + + public static final String AllHalfYearRegex = "\\b(?primer|1er|segundo|2do|[12](\\.)?º)\\s+semestre(\\s+(de\\s+)?({YearRegex}|{RelativeRegex}\\s+año))?\\b" + .replace("{YearRegex}", YearRegex) + .replace("{RelativeRegex}", RelativeRegex); + + public static final String EarlyPrefixRegex = "\\b(?(?m[aá]s\\s+temprano(\\s+(del?|en))?)|((comienzos?|inicios?|principios?|temprano)\\s+({OfPrepositionRegex}(\\s+d[ií]a)?)))(\\s+(el|las?|los?))?\\b" + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String MidPrefixRegex = "\\b(?(media[dn]os\\s+({OfPrepositionRegex})))(\\s+(el|las?|los?))?\\b" + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String LaterPrefixRegex = "\\b(?((fin(al)?(es)?|[uú]ltimos)\\s+({OfPrepositionRegex}))|(?m[aá]s\\s+tarde(\\s+(del?|en))?))(\\s+(el|las?|los?))?\\b" + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String PrefixPeriodRegex = "({EarlyPrefixRegex}|{MidPrefixRegex}|{LaterPrefixRegex})" + .replace("{EarlyPrefixRegex}", EarlyPrefixRegex) + .replace("{MidPrefixRegex}", MidPrefixRegex) + .replace("{LaterPrefixRegex}", LaterPrefixRegex); + + public static final String PrefixDayRegex = "\\b((?(comienzos?|inicios?|principios?|temprano))|(?mediados)|(?(fin((al)?es)?|m[aá]s\\s+tarde)))(\\s+(en|{OfPrepositionRegex}))?(\\s+([ae]l)(\\s+d[ií]a)?)?$" + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String CenturySuffixRegex = "(^siglo)\\b"; + + public static final String SeasonRegex = "\\b(?(([uú]ltim[oa]|est[ea]|el|la|(pr[oó]xim[oa]s?|siguiente)|{PrefixPeriodRegex})\\s+)?(?primavera|verano|otoño|invierno)((\\s+(del?|en)|\\s*,\\s*)?\\s+({YearRegex}|(?pr[oó]ximo|[uú]ltimo|este)\\s+año))?)\\b" + .replace("{YearRegex}", YearRegex) + .replace("{PrefixPeriodRegex}", PrefixPeriodRegex); + + public static final String WhichWeekRegex = "\\b(semana)(\\s*)(?5[0-3]|[1-4]\\d|0?[1-9])\\b"; + + public static final String WeekOfRegex = "((del?|el|la)\\s+)?(semana)(\\s*)({OfPrepositionRegex}|que\\s+(inicia|comienza)\\s+el|(que\\s+va|a\\s+partir)\\s+del)" + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String MonthOfRegex = "(mes)(\\s+)({OfPrepositionRegex})" + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String RangeUnitRegex = "\\b(?años?|mes(es)?|semanas?)\\b"; + + public static final String BeforeAfterRegex = "^[.]"; + + public static final String InConnectorRegex = "\\b(en)\\b"; + + public static final String SinceYearSuffixRegex = "^[.]"; + + public static final String WithinNextPrefixRegex = "\\b(dentro\\s+de)\\b"; + + public static final String TodayNowRegex = "\\b(hoy|ahora|este entonces)\\b"; + + public static final String FromRegex = "((\\bde(sde)?)(\\s*la(s)?)?)$"; + + public static final String BetweenRegex = "(\\bentre\\s*(la(s)?)?)"; + + public static final String WeekDayRegex = "\\b(?(domingos?|lunes|martes|mi[eé]rcoles|jueves|viernes|s[aá]bados?)\\b|(lun|mar|mi[eé]|jue|vie|s[aá]b|dom|lu|ma|mi|ju|vi|s[aá]|do)(\\.|\\b))(?!ñ)"; + + public static final String OnRegex = "((?<=\\b(e[ln])\\s+)|(\\be[ln]\\s+d[ií]a\\s+))({DayRegex}s?)(?![.,]\\d)\\b" + .replace("{DayRegex}", DayRegex); + + public static final String RelaxedOnRegex = "(?<=\\b(en|d?el)\\s+)((?10|11|12|13|14|15|16|17|18|19|1st|20|21|22|23|24|25|26|27|28|29|2|30|31|3|4|5|6|7|8|9)s?)(?![.,]\\d)\\b"; + + public static final String SpecialDayRegex = "\\b((el\\s+)?(d[ií]a\\s+antes\\s+de\\s+ayer|anteayer)|((el\\s+)?d[ií]a\\s+(despu[eé]s\\s+)?de\\s+mañana|pasado\\s+mañana)|(el\\s)?d[ií]a\\s+(siguiente|anterior)|(el\\s)?pr[oó]ximo\\s+d[ií]a|(el\\s+)?[uú]ltimo\\s+d[ií]a|(d)?el\\s+d[ií]a(?!\\s+d)|ayer|mañana|hoy)\\b"; + + public static final String SpecialDayWithNumRegex = "^[.]"; + + public static final String FlexibleDayRegex = "(?([A-Za-z]+\\s)?({WrittenDayRegex}|{DayRegex}))" + .replace("{WrittenDayRegex}", WrittenDayRegex) + .replace("{DayRegex}", DayRegex); + + public static final String ForTheRegex = "\\b((((?<=para\\s+el\\s+){FlexibleDayRegex})|((?\\s*(,|\\.(?![º°ª])|!|\\?|-|$))(?!\\d))" + .replace("{FlexibleDayRegex}", FlexibleDayRegex) + .replace("{MonthRegex}", MonthRegex) + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String WeekDayAndDayOfMonthRegex = "\\b{WeekDayRegex}\\s+((el\\s+(d[ií]a\\s+)?){FlexibleDayRegex})\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{FlexibleDayRegex}", FlexibleDayRegex); + + public static final String WeekDayAndDayRegex = "\\b{WeekDayRegex}\\s+({DayRegex}|{WrittenDayRegex})(?!([-:/]|\\.\\d|(\\s+({AmDescRegex}|{PmDescRegex}|{OclockRegex}))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{DayRegex}", DayRegex) + .replace("{WrittenDayRegex}", WrittenDayRegex) + .replace("{AmDescRegex}", AmDescRegex) + .replace("{PmDescRegex}", PmDescRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String WeekDayOfMonthRegex = "(?(el\\s+)?(?primera?|1era?|segund[ao]|2d[ao]|tercera?|3era?|cuart[ao]|4t[ao]|quint[ao]|5t[ao]|((1|2|3|4|5)(\\.)?[ºª])|[uú]ltim[ao])\\s+(semana\\s+{MonthSuffixRegex}\\s+el\\s+{WeekDayRegex}|{WeekDayRegex}\\s+{MonthSuffixRegex}))" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String RelativeWeekDayRegex = "^[.]"; + + public static final String AmbiguousRangeModifierPrefix = "^[.]"; + + public static final String NumberEndingPattern = "^[.]"; + + public static final String DateTokenPrefix = "en "; + + public static final String TimeTokenPrefix = "a las "; + + public static final String TokenBeforeDate = "el "; + + public static final String TokenBeforeTime = "a las "; + + public static final String HalfTokenRegex = "^((y\\s+)?media)"; + + public static final String QuarterTokenRegex = "^((y\\s+)?cuarto|(?menos\\s+cuarto))"; + + public static final String PastTokenRegex = "\\b(pasad[ao]s(\\s+(de\\s+)?las)?)$"; + + public static final String ToTokenRegex = "\\b((para|antes)(\\s+(de\\s+)?las?)|(?^menos))$"; + + public static final String SpecialDateRegex = "(?<=\\b(en)\\s+el\\s+){DayRegex}\\b" + .replace("{DayRegex}", DayRegex); + + public static final String OfMonthRegex = "^\\s*((d[ií]a\\s+)?d[eo]\\s+)?{MonthSuffixRegex}" + .replace("{MonthSuffixRegex}", MonthSuffixRegex); + + public static final String MonthEndRegex = "({MonthRegex}\\s*(el)?\\s*$)" + .replace("{MonthRegex}", MonthRegex); + + public static final String WeekDayEnd = "{WeekDayRegex}\\s*,?\\s*$" + .replace("{WeekDayRegex}", WeekDayRegex); + + public static final String WeekDayStart = "^[\\.]"; + + public static final String DateYearRegex = "(?{YearRegex}|(?cero|una|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez|once|doce)\\b"; + + public static final String MinuteNumRegex = "(?uno?|d[óo]s|tr[eé]s|cuatro|cinco|s[eé]is|siete|ocho|nueve|diez|once|doce|trece|catorce|quince|diecis[eé]is|diecisiete|dieciocho|diecinueve|veinte|treinta|cuarenta|cincuenta)"; + + public static final String DeltaMinuteNumRegex = "(?uno?|d[óo]s|tr[eé]s|cuatro|cinco|s[eé]is|siete|ocho|nueve|diez|once|doce|trece|catorce|quince|diecis[eé]is|diecisiete|dieciocho|diecinueve|veinte|treinta|cuarenta|cincuenta)"; + + public static final String PmRegex = "(?((por|de|a|en)\\s+la)\\s+(tarde|noche))"; + + public static final String AmRegex = "(?((por|de|a|en)\\s+la)\\s+(mañana|madrugada))"; + + public static final String AmTimeRegex = "(?(esta|(por|de|a|en)\\s+la)\\s+(mañana|madrugada))"; + + public static final String PmTimeRegex = "(?(esta|(por|de|a|en)\\s+la)\\s+(tarde|noche))"; + + public static final String NightTimeRegex = "(noche)"; + + public static final String LastNightTimeRegex = "(anoche)"; + + public static final String NowTimeRegex = "(ahora|mismo|momento)"; + + public static final String RecentlyTimeRegex = "(mente)"; + + public static final String AsapTimeRegex = "(posible|pueda[ns]?|podamos)"; + + public static final String LessThanOneHour = "(?((\\s+y\\s+)?cuarto|(\\s*)menos cuarto|(\\s+y\\s+)media|{BaseDateTime.DeltaMinuteRegex}(\\s+(minutos?|mins?))|{DeltaMinuteNumRegex}(\\s+(minutos?|mins?))))" + .replace("{BaseDateTime.DeltaMinuteRegex}", BaseDateTime.DeltaMinuteRegex) + .replace("{DeltaMinuteNumRegex}", DeltaMinuteNumRegex); + + public static final String TensTimeRegex = "(?diez|veint(i|e)|treinta|cuarenta|cincuenta)"; + + public static final String WrittenTimeRegex = "(?{HourNumRegex}\\s*((y|(?menos))\\s+)?(({TensTimeRegex}(\\s*y\\s+)?)?{MinuteNumRegex}))" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex) + .replace("{TensTimeRegex}", TensTimeRegex); + + public static final String TimePrefix = "(?{LessThanOneHour}(\\s+(pasad[ao]s)\\s+(de\\s+las|las)?|\\s+(para|antes\\s+de)?\\s+(las?))?)" + .replace("{LessThanOneHour}", LessThanOneHour); + + public static final String TimeSuffix = "(?({LessThanOneHour}\\s+)?({AmRegex}|{PmRegex}|{OclockRegex}))" + .replace("{LessThanOneHour}", LessThanOneHour) + .replace("{AmRegex}", AmRegex) + .replace("{PmRegex}", PmRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String GeneralDescRegex = "({DescRegex}|(?{AmRegex}|{PmRegex}))" + .replace("{DescRegex}", DescRegex) + .replace("{AmRegex}", AmRegex) + .replace("{PmRegex}", PmRegex); + + public static final String BasicTime = "(?{WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}:{BaseDateTime.MinuteRegex}(:{BaseDateTime.SecondRegex})?|{BaseDateTime.HourRegex})" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex); + + public static final String MidTimeRegex = "(?((?media\\s*noche)|(?media\\s*mañana)|(?media\\s*tarde)|(?medio\\s*d[ií]a)))"; + + public static final String AtRegex = "\\b((?<=\\b((a|de(sde)?)\\s+las?|al)\\s+)(({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})\\b(\\s*\\bh\\b)?(DescRegex)?|{MidTimeRegex})|{MidTimeRegex})" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{DescRegex}", DescRegex) + .replace("{MidTimeRegex}", MidTimeRegex); + + public static final String ConnectNumRegex = "({BaseDateTime.HourRegex}(?[0-5][0-9])\\s*{DescRegex})" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegexWithDotConnector = "({BaseDateTime.HourRegex}\\.{BaseDateTime.MinuteRegex})" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex); + + public static final String TimeRegex1 = "(\\b{TimePrefix}\\s+)?({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex})\\s*({DescRegex}|\\s*\\bh\\b)" + .replace("{TimePrefix}", TimePrefix) + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex2 = "(\\b{TimePrefix}\\s+)?(t)?{BaseDateTime.HourRegex}(\\s*)?:(\\s*)?{BaseDateTime.MinuteRegex}((\\s*)?:(\\s*)?{BaseDateTime.SecondRegex})?((\\s*{DescRegex})|\\b)" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{BaseDateTime.SecondRegex}", BaseDateTime.SecondRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex3 = "\\b(({TimePrefix}\\s+)?{TimeRegexWithDotConnector}(\\s*({DescRegex}|{TimeSuffix}|\\bh\\b))|((las\\s+{TimeRegexWithDotConnector})(?!\\s*(por\\s+cien(to)?|%))(\\s*({DescRegex}|{TimeSuffix}|\\bh\\b)|\\b)))" + .replace("{TimePrefix}", TimePrefix) + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{DescRegex}", DescRegex) + .replace("{TimeTokenPrefix}", TimeTokenPrefix) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex4 = "\\b(({DescRegex}?)|({BasicTime}\\s*)?({GeneralDescRegex}?)){TimePrefix}(\\s*({HourNumRegex}|{BaseDateTime.HourRegex}))?(\\s+{TensTimeRegex}(\\s*(y\\s+)?{MinuteNumRegex})?)?(\\s*({OclockRegex}|{DescRegex})|\\b)" + .replace("{DescRegex}", DescRegex) + .replace("{GeneralDescRegex}", GeneralDescRegex) + .replace("{BasicTime}", BasicTime) + .replace("{TimePrefix}", TimePrefix) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TensTimeRegex}", TensTimeRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex) + .replace("{OclockRegex}", OclockRegex); + + public static final String TimeRegex5 = "\\b({TimePrefix}|{BasicTime}{TimePrefix})\\s+(\\s*{DescRegex})?{BasicTime}?\\s*{TimeSuffix}\\b" + .replace("{TimePrefix}", TimePrefix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex6 = "({BasicTime}(\\s*{DescRegex})?\\s+{TimeSuffix}\\b)" + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex) + .replace("{TimeSuffix}", TimeSuffix); + + public static final String TimeRegex7 = "\\b{TimeSuffix}\\s+a\\s+las\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffix}", TimeSuffix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex8 = "\\b{TimeSuffix}\\s+{BasicTime}((\\s*{DescRegex})|\\b)" + .replace("{TimeSuffix}", TimeSuffix) + .replace("{BasicTime}", BasicTime) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex9 = "\\b(?{HourNumRegex}\\s+({TensTimeRegex}\\s*)(y\\s+)?{MinuteNumRegex}?)\\b" + .replace("{HourNumRegex}", HourNumRegex) + .replace("{TensTimeRegex}", TensTimeRegex) + .replace("{MinuteNumRegex}", MinuteNumRegex); + + public static final String TimeRegex10 = "(a\\s+la|al)\\s+(madrugada|mañana|tarde|noche)"; + + public static final String TimeRegex11 = "\\b({WrittenTimeRegex})(\\s+{DescRegex})?\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeRegex12 = "(\\b{TimePrefix}\\s+)?{BaseDateTime.HourRegex}(\\s*h\\s*){BaseDateTime.MinuteRegex}(\\s*{DescRegex})?" + .replace("{TimePrefix}", TimePrefix) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{BaseDateTime.MinuteRegex}", BaseDateTime.MinuteRegex) + .replace("{DescRegex}", DescRegex); + + public static final String PrepositionRegex = "(?^(,\\s*)?(a(l)?|en|de(l)?)?(\\s*(la(s)?|el|los))?$)"; + + public static final String LaterEarlyRegex = "((?temprano)|(?fin(al)?(\\s+de)?|m[aá]s\\s+tarde))"; + + public static final String NowRegex = "\\b(?(justo\\s+)?ahora(\\s+mismo)?|en\\s+este\\s+momento|tan\\s+pronto\\s+como\\s+sea\\s+posible|tan\\s+pronto\\s+como\\s+(pueda|puedas|podamos|puedan)|lo\\s+m[aá]s\\s+pronto\\s+posible|recientemente|previamente|este entonces)\\b"; + + public static final String SuffixRegex = "^\\s*(((y|a|en|por)\\s+la|al)\\s+)?(mañana|madrugada|medio\\s*d[ií]a|(?(({LaterEarlyRegex}\\s+)((del?|en|por)(\\s+(el|los?|las?))?\\s+)?)?(mañana|madrugada|pasado\\s+(el\\s+)?medio\\s?d[ií]a|(?mañana|madrugada|(?pasado\\s+(el\\s+)?medio\\s?d[ií]a|tarde|noche))\\b"; + + public static final String PeriodTimeOfDayRegex = "\\b((en\\s+(el|la|lo)?\\s+)?({LaterEarlyRegex}\\s+)?(est[ae]\\s+)?{DateTimeTimeOfDayRegex})\\b" + .replace("{DateTimeTimeOfDayRegex}", DateTimeTimeOfDayRegex) + .replace("{LaterEarlyRegex}", LaterEarlyRegex); + + public static final String PeriodSpecificTimeOfDayRegex = "\\b(({LaterEarlyRegex}\\s+)?est[ae]\\s+{DateTimeTimeOfDayRegex}|({StrictRelativeRegex}\\s+{PeriodTimeOfDayRegex})|anoche)\\b" + .replace("{PeriodTimeOfDayRegex}", PeriodTimeOfDayRegex) + .replace("{StrictRelativeRegex}", StrictRelativeRegex) + .replace("{DateTimeTimeOfDayRegex}", DateTimeTimeOfDayRegex) + .replace("{LaterEarlyRegex}", LaterEarlyRegex); + + public static final String UnitRegex = "(?años?|(bi|tri|cuatri|se)mestre|mes(es)?|semanas?|fin(es)?\\s+de\\s+semana|finde|d[ií]as?|horas?|hra?s?|hs?|minutos?|mins?|segundos?|segs?|noches?)\\b"; + + public static final String ConnectorRegex = "^(,|t|(para|y|a|en|por) las?|(\\s*,\\s*)?(cerca|alrededor) de las?)$"; + + public static final String TimeHourNumRegex = "(?veint(i(uno|dos|tres|cuatro)|e)|cero|uno|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez|once|doce|trece|catorce|quince|dieci(s([eé])is|siete|ocho|nueve))"; + + public static final String PureNumFromTo = "((\\b(desde|de)\\s+(la(s)?\\s+)?)?({BaseDateTime.HourRegex}|{TimeHourNumRegex})(?!\\s+al?\\b)(\\s*(?{DescRegex}))?|(\\b(desde|de)\\s+(la(s)?\\s+)?)({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?)\\s*{TillRegex}\\s*({BaseDateTime.HourRegex}|{TimeHourNumRegex})\\s*(?{PmRegex}|{AmRegex}|{DescRegex})?" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{TillRegex}", TillRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex); + + public static final String PureNumBetweenAnd = "(\\bentre\\s+(la(s)?\\s+)?)(({BaseDateTime.TwoDigitHourRegex}{BaseDateTime.TwoDigitMinuteRegex})|{BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?\\s*{RangeConnectorRegex}\\s*(({BaseDateTime.TwoDigitHourRegex}{BaseDateTime.TwoDigitMinuteRegex})|{BaseDateTime.HourRegex}|{TimeHourNumRegex})\\s*(?{PmRegex}|{AmRegex}|{DescRegex})?" + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{PmRegex}", PmRegex) + .replace("{AmRegex}", AmRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{BaseDateTime.TwoDigitHourRegex}", BaseDateTime.TwoDigitHourRegex) + .replace("{BaseDateTime.TwoDigitMinuteRegex}", BaseDateTime.TwoDigitMinuteRegex); + + public static final String SpecificTimeFromTo = "({RangePrefixRegex}\\s+)?(?(({TimeRegex2}|{TimeRegexWithDotConnector}(\\s*{DescRegex})?)|({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?))\\s*{TillRegex}\\s*(?(({TimeRegex2}|{TimeRegexWithDotConnector}(\\s*{DescRegex})?)|({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?))" + .replace("{TimeRegex2}", TimeRegex2) + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{TillRegex}", TillRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{DescRegex}", DescRegex) + .replace("{RangePrefixRegex}", RangePrefixRegex); + + public static final String SpecificTimeBetweenAnd = "({BetweenRegex}\\s+)(?(({TimeRegex1}|{TimeRegex2}|{TimeRegexWithDotConnector}(\\s*{DescRegex})?)|({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?))\\s*{RangeConnectorRegex}\\s*(?(({TimeRegex1}|{TimeRegex2}|{TimeRegexWithDotConnector}(\\s*{DescRegex})?)|({BaseDateTime.HourRegex}|{TimeHourNumRegex})(\\s*(?{DescRegex}))?))" + .replace("{BetweenRegex}", BetweenRegex) + .replace("{TimeRegex1}", TimeRegex1) + .replace("{TimeRegex2}", TimeRegex2) + .replace("{TimeRegexWithDotConnector}", TimeRegexWithDotConnector) + .replace("{RangeConnectorRegex}", RangeConnectorRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{TimeHourNumRegex}", TimeHourNumRegex) + .replace("{DescRegex}", DescRegex); + + public static final String TimeUnitRegex = "([^A-Za-z]{1,}|\\b)(?horas?|h|minutos?|mins?|segundos?|se[cg]s?)\\b"; + + public static final String TimeFollowedUnit = "^\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String TimeNumberCombinedWithUnit = "\\b(?\\d+(\\,\\d*)?)\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String DateTimePeriodNumberCombinedWithUnit = "\\b(?\\d+(\\.\\d*)?)\\s*{TimeUnitRegex}" + .replace("{TimeUnitRegex}", TimeUnitRegex); + + public static final String PeriodTimeOfDayWithDateRegex = "\\b(((y|a|en|por)\\s+(la\\s+)?|al\\s+)?((((?primeras\\s+horas\\s+)|(?(últimas|altas)\\s+horas\\s+))(de\\s+la\\s+)?|{LaterEarlyRegex}\\s+(est[ae]\\s+)?)?(?(mañana|madrugada|pasado\\s+(el\\s+)?medio\\s?d[ií]a|(?\\s*(y)\\s+((un[ao]?)\\s+)?(?media|cuarto))"; + + public static final String FollowedUnit = "^\\s*{UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String DurationNumberCombinedWithUnit = "\\b(?\\d+(\\,\\d*)?){UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String AnUnitRegex = "\\b(una?|otr[ao])\\s+{UnitRegex}" + .replace("{UnitRegex}", UnitRegex); + + public static final String DuringRegex = "^[.]"; + + public static final String AllRegex = "\\b(?tod[oa]?\\s+(el|la)\\s+(?año|mes|semana|d[ií]a)|((una?|el|la)\\s+)?(?año|mes|semana|d[ií]a)\\s+enter[ao])\\b"; + + public static final String HalfRegex = "\\b(?medi[oa]\\s+(?ano|mes|semana|d[íi]a|hora))\\b"; + + public static final String ConjunctionRegex = "^[.]"; + + public static final String InexactNumberRegex = "\\b(pocos?|algo|vari[ao]s|algun[ao]s|un[ao]s)\\b"; + + public static final String InexactNumberUnitRegex = "({InexactNumberRegex})\\s+{UnitRegex}" + .replace("{InexactNumberRegex}", InexactNumberRegex) + .replace("{UnitRegex}", UnitRegex); + + public static final String HolidayRegex1 = "\\b(?viernes santo|mi[eé]rcoles de ceniza|martes de carnaval|d[ií]a (de|de los) presidentes?|clebraci[oó]n de mao|año nuevo chino|año nuevo|noche vieja|(festividad de )?los mayos|d[ií]a de los inocentes|navidad|noche buena|d[ií]a de acci[oó]n de gracias|acci[oó]n de gracias|yuandan|halloween|noches de brujas|pascuas)(\\s+(del?\\s+)?({YearRegex}|(?(pr[oó]xim[oa]?|est[ea]|[uú]ltim[oa]?|en))\\s+año))?\\b" + .replace("{YearRegex}", YearRegex); + + public static final String HolidayRegex2 = "\\b(?(d[ií]a( del?( la)?)? )?(martin luther king|todos los santos|blanco|san patricio|san valent[ií]n|san jorge|cinco de mayo|independencia|raza|trabajador))(\\s+(del?\\s+)?({YearRegex}|(?(pr[oó]xim[oa]?|est[ea]|[uú]ltim[oa]?|en))\\s+año))?\\b" + .replace("{YearRegex}", YearRegex); + + public static final String HolidayRegex3 = "\\b(?(d[ií]a( internacional)?( del?( l[ao]s?)?)? )(trabajador(es)?|madres?|padres?|[aá]rbol|mujer(es)?|solteros?|niños?|marmota|san valent[ií]n|maestro))(\\s+(del?\\s+)?({YearRegex}|(?(pr[oó]xim[oa]?|est[ea]|[uú]ltim[oa]?|en))\\s+año))?\\b" + .replace("{YearRegex}", YearRegex); + + public static final String BeforeRegex = "(\\b((ante(s|rior)|m[aá]s\\s+temprano|no\\s+m[aá]s\\s+tard(e|ar)|(?tan\\s+tarde\\s+como))(\\s+(del?|a|que)(\\s+(el|las?|los?))?)?)|(?)((?<\\s*=)|<))"; + + public static final String AfterRegex = "((\\b(despu[eé]s|(año\\s+)?posterior|m[aá]s\\s+tarde|a\\s+primeros)(\\s*(del?|en|a)(\\s+(el|las?|los?))?)?|(empi?en?zando|comenzando)(\\s+(el|las?|los?))?)\\b|(?>\\s*=)|>))"; + + public static final String SinceRegex = "\\b(((cualquier\\s+tiempo\\s+)?(desde|a\\s+partir\\s+del?)|tan\\s+(temprano|pronto)\\s+como(\\s+(de|a))?)(\\s+(el|las?|los?))?)\\b"; + + public static final String SinceRegexExp = "({SinceRegex}|\\bde\\b)" + .replace("{SinceRegex}", SinceRegex); + + public static final String AroundRegex = "(?:\\b(?:cerca|alrededor|aproximadamente)(\\s+(de\\s+(las?|el)|del?))?\\s*\\b)"; + + public static final String PeriodicRegex = "\\b(?a\\s*diario|diaria(s|mente)|(bi|tri)?(semanal|quincenal|mensual|semestral|anual)(es|mente)?)\\b"; + + public static final String EachExpression = "\\b(cada|tod[oa]s\\s*(l[oa]s)?)\\b\\s*(?!\\s*l[oa]\\b)"; + + public static final String EachUnitRegex = "(?({EachExpression})\\s*({UnitRegex}|(?fin(es)?\\s+de\\s+semana|finde)\\b))" + .replace("{EachExpression}", EachExpression) + .replace("{UnitRegex}", UnitRegex); + + public static final String EachPrefixRegex = "(?({EachExpression})\\s*$)" + .replace("{EachExpression}", EachExpression); + + public static final String EachDayRegex = "\\s*({EachExpression})\\s*d[ií]as\\s*\\b" + .replace("{EachExpression}", EachExpression); + + public static final String BeforeEachDayRegex = "({EachExpression})\\s*d[ií]as(\\s+a\\s+las?)?\\s*\\b" + .replace("{EachExpression}", EachExpression); + + public static final String SetEachRegex = "(?({EachExpression})\\s*)" + .replace("{EachExpression}", EachExpression); + + public static final String LaterEarlyPeriodRegex = "\\b(({PrefixPeriodRegex})\\s+(?{OneWordPeriodRegex}|(?{BaseDateTime.FourDigitYearRegex}))|({UnspecificEndOfRangeRegex}))\\b" + .replace("{OneWordPeriodRegex}", OneWordPeriodRegex) + .replace("{UnspecificEndOfRangeRegex}", UnspecificEndOfRangeRegex) + .replace("{PrefixPeriodRegex}", PrefixPeriodRegex) + .replace("{BaseDateTime.FourDigitYearRegex}", BaseDateTime.FourDigitYearRegex); + + public static final String RelativeWeekRegex = "(((la|el)\\s+)?(((est[ae]|pr[oó]xim[oa]|[uú]ltim(o|as|os))\\s+semanas?)|(semanas?\\s+(que\\s+viene|pasad[oa]))))"; + + public static final String WeekWithWeekDayRangeRegex = "\\b((({RelativeWeekRegex})((\\s+entre\\s+{WeekDayRegex}\\s+y\\s+{WeekDayRegex})|(\\s+de\\s+{WeekDayRegex}\\s+a\\s+{WeekDayRegex})))|((entre\\s+{WeekDayRegex}\\s+y\\s+{WeekDayRegex})|(de\\s+{WeekDayRegex}\\s+a\\s+{WeekDayRegex})){OfPrepositionRegex}\\s+{RelativeWeekRegex})\\b" + .replace("{RelativeWeekRegex}", RelativeWeekRegex) + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{OfPrepositionRegex}", OfPrepositionRegex); + + public static final String GeneralEndingRegex = "^\\s*((\\.,)|\\.|,|!|\\?)?\\s*$"; + + public static final String MiddlePauseRegex = "^[.]"; + + public static final String PrefixArticleRegex = "\\b(e[ln]\\s+(d[ií]a\\s+)?)"; + + public static final String OrRegex = "^[.]"; + + public static final String SpecialYearTermsRegex = "\\b(años?\\s+({SpecialYearPrefixes}\\s+)?(de\\s+)?)" + .replace("{SpecialYearPrefixes}", SpecialYearPrefixes); + + public static final String YearPlusNumberRegex = "\\b({SpecialYearTermsRegex}((?(\\d{2,4}))|{FullTextYearRegex}))\\b" + .replace("{FullTextYearRegex}", FullTextYearRegex) + .replace("{SpecialYearTermsRegex}", SpecialYearTermsRegex); + + public static final String NumberAsTimeRegex = "^[.]"; + + public static final String TimeBeforeAfterRegex = "\\b((?<=\\b(antes|no\\s+m[aá]s\\s+tard(e|ar)\\s+(de|a\\s+las?)|por| después)\\s+)({WrittenTimeRegex}|{HourNumRegex}|{BaseDateTime.HourRegex}|{MidTimeRegex}))\\b" + .replace("{WrittenTimeRegex}", WrittenTimeRegex) + .replace("{HourNumRegex}", HourNumRegex) + .replace("{BaseDateTime.HourRegex}", BaseDateTime.HourRegex) + .replace("{MidTimeRegex}", MidTimeRegex); + + public static final String DateNumberConnectorRegex = "^[.]"; + + public static final String CenturyRegex = "^[.]"; + + public static final String DecadeRegex = "(?diez|veinte|treinta|cuarenta|cincuenta|se[st]enta|ochenta|noventa)"; + + public static final String DecadeWithCenturyRegex = "(los\\s+)?((((d[ée]cada(\\s+de)?)\\s+)(((?\\d|1\\d|2\\d)?(?\\d0))))|a[ñn]os\\s+((((dos\\s+)?mil\\s+)?({WrittenOneHundredToNineHundredRegex}\\s+)?{DecadeRegex})|((dos\\s+)?mil\\s+)?({WrittenOneHundredToNineHundredRegex})(\\s+{DecadeRegex}?)|((dos\\s+)?mil)(\\s+{WrittenOneHundredToNineHundredRegex}\\s+)?{DecadeRegex}?))" + .replace("{WrittenOneHundredToNineHundredRegex}", WrittenOneHundredToNineHundredRegex) + .replace("{DecadeRegex}", DecadeRegex); + + public static final String RelativeDecadeRegex = "\\b(((el|las?)\\s+)?{RelativeRegex}\\s+(((?[\\d]+)|{WrittenOneToNineRegex})\\s+)?d[eé]cadas?)\\b" + .replace("{RelativeRegex}", RelativeRegex) + .replace("{WrittenOneToNineRegex}", WrittenOneToNineRegex); + + public static final String ComplexDatePeriodRegex = "(?:((de(sde)?)\\s+)?(?.+)\\s*({StrictTillRegex})\\s*(?.+)|((entre)\\s+)(?.+)\\s*({RangeConnectorRegex})\\s*(?.+))" + .replace("{StrictTillRegex}", StrictTillRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex); + + public static final String AmbiguousPointRangeRegex = "^(mar\\.?)$"; + + public static final String YearSuffix = "((,|\\sde)?\\s*({YearRegex}|{FullTextYearRegex}))" + .replace("{YearRegex}", YearRegex) + .replace("{FullTextYearRegex}", FullTextYearRegex); + + public static final String AgoRegex = "\\b(antes\\s+de\\s+(?hoy|ayer|mañana)|antes)\\b"; + + public static final String LaterRegex = "\\b(despu[eé]s(?!\\s+de\\b)|desde\\s+ahora|a\\s+partir\\s+de\\s+(?hoy|ayer|mañana))\\b"; + + public static final String Tomorrow = "mañana"; + + public static final ImmutableMap UnitMap = ImmutableMap.builder() + .put("años", "Y") + .put("año", "Y") + .put("meses", "MON") + .put("mes", "MON") + .put("trimestre", "3MON") + .put("trimestres", "3MON") + .put("cuatrimestre", "4MON") + .put("cuatrimestres", "4MON") + .put("semestre", "6MON") + .put("semestres", "6MON") + .put("bimestre", "2MON") + .put("bimestres", "2MON") + .put("semanas", "W") + .put("semana", "W") + .put("fin de semana", "WE") + .put("fines de semana", "WE") + .put("finde", "WE") + .put("dias", "D") + .put("dia", "D") + .put("días", "D") + .put("día", "D") + .put("jornada", "D") + .put("noche", "D") + .put("noches", "D") + .put("horas", "H") + .put("hora", "H") + .put("hrs", "H") + .put("hras", "H") + .put("hra", "H") + .put("hr", "H") + .put("h", "H") + .put("minutos", "M") + .put("minuto", "M") + .put("mins", "M") + .put("min", "M") + .put("segundos", "S") + .put("segundo", "S") + .put("segs", "S") + .put("seg", "S") + .build(); + + public static final ImmutableMap UnitValueMap = ImmutableMap.builder() + .put("años", 31536000L) + .put("año", 31536000L) + .put("meses", 2592000L) + .put("mes", 2592000L) + .put("semanas", 604800L) + .put("semana", 604800L) + .put("fin de semana", 172800L) + .put("fines de semana", 172800L) + .put("finde", 172800L) + .put("dias", 86400L) + .put("dia", 86400L) + .put("días", 86400L) + .put("día", 86400L) + .put("noche", 86400L) + .put("noches", 86400L) + .put("horas", 3600L) + .put("hora", 3600L) + .put("hrs", 3600L) + .put("hras", 3600L) + .put("hra", 3600L) + .put("hr", 3600L) + .put("h", 3600L) + .put("minutos", 60L) + .put("minuto", 60L) + .put("mins", 60L) + .put("min", 60L) + .put("segundos", 1L) + .put("segundo", 1L) + .put("segs", 1L) + .put("seg", 1L) + .build(); + + public static final ImmutableMap SpecialYearPrefixesMap = ImmutableMap.builder() + .put("fiscal", "FY") + .put("escolar", "SY") + .build(); + + public static final ImmutableMap SeasonMap = ImmutableMap.builder() + .put("primavera", "SP") + .put("verano", "SU") + .put("otoño", "FA") + .put("invierno", "WI") + .build(); + + public static final ImmutableMap SeasonValueMap = ImmutableMap.builder() + .put("SP", 3) + .put("SU", 6) + .put("FA", 9) + .put("WI", 12) + .build(); + + public static final ImmutableMap CardinalMap = ImmutableMap.builder() + .put("primer", 1) + .put("primero", 1) + .put("primera", 1) + .put("1er", 1) + .put("1ro", 1) + .put("1ra", 1) + .put("1.º", 1) + .put("1º", 1) + .put("1ª", 1) + .put("segundo", 2) + .put("segunda", 2) + .put("2do", 2) + .put("2da", 2) + .put("2.º", 2) + .put("2º", 2) + .put("2ª", 2) + .put("tercer", 3) + .put("tercero", 3) + .put("tercera", 3) + .put("3er", 3) + .put("3ro", 3) + .put("3ra", 3) + .put("3.º", 3) + .put("3º", 3) + .put("3ª", 3) + .put("cuarto", 4) + .put("cuarta", 4) + .put("4to", 4) + .put("4ta", 4) + .put("4.º", 4) + .put("4º", 4) + .put("4ª", 4) + .put("quinto", 5) + .put("quinta", 5) + .put("5to", 5) + .put("5ta", 5) + .put("5.º", 5) + .put("5º", 5) + .put("5ª", 5) + .build(); + + public static final ImmutableMap DayOfWeek = ImmutableMap.builder() + .put("lunes", 1) + .put("martes", 2) + .put("miercoles", 3) + .put("miércoles", 3) + .put("jueves", 4) + .put("viernes", 5) + .put("sabado", 6) + .put("sábado", 6) + .put("domingo", 0) + .put("dom", 0) + .put("lun", 1) + .put("mar", 2) + .put("mie", 3) + .put("mié", 3) + .put("jue", 4) + .put("vie", 5) + .put("sab", 6) + .put("sáb", 6) + .put("dom.", 0) + .put("lun.", 1) + .put("mar.", 2) + .put("mie.", 3) + .put("mié.", 3) + .put("jue.", 4) + .put("vie.", 5) + .put("sab.", 6) + .put("sáb.", 6) + .put("do", 0) + .put("lu", 1) + .put("ma", 2) + .put("mi", 3) + .put("ju", 4) + .put("vi", 5) + .put("sa", 6) + .build(); + + public static final ImmutableMap MonthOfYear = ImmutableMap.builder() + .put("enero", 1) + .put("febrero", 2) + .put("marzo", 3) + .put("abril", 4) + .put("mayo", 5) + .put("junio", 6) + .put("julio", 7) + .put("agosto", 8) + .put("septiembre", 9) + .put("setiembre", 9) + .put("octubre", 10) + .put("noviembre", 11) + .put("diciembre", 12) + .put("ene", 1) + .put("feb", 2) + .put("mar", 3) + .put("abr", 4) + .put("may", 5) + .put("jun", 6) + .put("jul", 7) + .put("ago", 8) + .put("sept", 9) + .put("sep", 9) + .put("set", 9) + .put("oct", 10) + .put("nov", 11) + .put("dic", 12) + .put("ene.", 1) + .put("feb.", 2) + .put("mar.", 3) + .put("abr.", 4) + .put("may.", 5) + .put("jun.", 6) + .put("jul.", 7) + .put("ago.", 8) + .put("sept.", 9) + .put("sep.", 9) + .put("set.", 9) + .put("oct.", 10) + .put("nov.", 11) + .put("dic.", 12) + .put("1", 1) + .put("2", 2) + .put("3", 3) + .put("4", 4) + .put("5", 5) + .put("6", 6) + .put("7", 7) + .put("8", 8) + .put("9", 9) + .put("10", 10) + .put("11", 11) + .put("12", 12) + .put("01", 1) + .put("02", 2) + .put("03", 3) + .put("04", 4) + .put("05", 5) + .put("06", 6) + .put("07", 7) + .put("08", 8) + .put("09", 9) + .build(); + + public static final ImmutableMap Numbers = ImmutableMap.builder() + .put("cero", 0) + .put("un", 1) + .put("una", 1) + .put("uno", 1) + .put("dos", 2) + .put("dós", 2) + .put("tres", 3) + .put("trés", 3) + .put("cuatro", 4) + .put("cinco", 5) + .put("seis", 6) + .put("séis", 6) + .put("siete", 7) + .put("ocho", 8) + .put("nueve", 9) + .put("diez", 10) + .put("once", 11) + .put("doce", 12) + .put("docena", 12) + .put("docenas", 12) + .put("trece", 13) + .put("catorce", 14) + .put("quince", 15) + .put("dieciseis", 16) + .put("dieciséis", 16) + .put("diecisiete", 17) + .put("dieciocho", 18) + .put("diecinueve", 19) + .put("veinte", 20) + .put("veinti", 20) + .put("ventiuna", 21) + .put("ventiuno", 21) + .put("veintiun", 21) + .put("veintiún", 21) + .put("veintiuno", 21) + .put("veintiuna", 21) + .put("veintidos", 22) + .put("veintidós", 22) + .put("veintitres", 23) + .put("veintitrés", 23) + .put("veinticuatro", 24) + .put("veinticinco", 25) + .put("veintiseis", 26) + .put("veintiséis", 26) + .put("veintisiete", 27) + .put("veintiocho", 28) + .put("veintinueve", 29) + .put("treinta", 30) + .put("cuarenta", 40) + .put("cincuenta", 50) + .build(); + + public static final ImmutableMap HolidayNames = ImmutableMap.builder() + .put("padres", new String[]{"diadelpadre"}) + .put("madres", new String[]{"diadelamadre"}) + .put("acciondegracias", new String[]{"diadegracias", "diadeacciondegracias", "acciondegracias"}) + .put("trabajador", new String[]{"diadeltrabajador", "diainternacionaldelostrabajadores"}) + .put("delaraza", new String[]{"diadelaraza", "diadeladiversidadcultural"}) + .put("memoria", new String[]{"diadelamemoria"}) + .put("pascuas", new String[]{"diadepascuas", "pascuas"}) + .put("navidad", new String[]{"navidad", "diadenavidad"}) + .put("nochebuena", new String[]{"diadenochebuena", "nochebuena"}) + .put("añonuevo", new String[]{"añonuevo", "diadeañonuevo"}) + .put("nochevieja", new String[]{"nochevieja", "diadenochevieja"}) + .put("yuandan", new String[]{"yuandan"}) + .put("earthday", new String[]{"diadelatierra"}) + .put("maestro", new String[]{"diadelmaestro"}) + .put("todoslossantos", new String[]{"todoslossantos"}) + .put("niño", new String[]{"diadelniño"}) + .put("mujer", new String[]{"diadelamujer"}) + .put("independencia", new String[]{"diadelaindependencia", "diadeindependencia", "independencia"}) + .build(); + + public static final ImmutableMap VariableHolidaysTimexDictionary = ImmutableMap.builder() + .put("padres", "-06-WXX-7-3") + .put("madres", "-05-WXX-7-2") + .put("acciondegracias", "-11-WXX-4-4") + .put("delaraza", "-10-WXX-1-2") + .put("memoria", "-03-WXX-2-4") + .build(); + + public static final ImmutableMap DoubleNumbers = ImmutableMap.builder() + .put("mitad", 0.5D) + .put("cuarto", 0.25D) + .build(); + + public static final String UpcomingPrefixRegex = "((este\\s+))"; + + public static final String NextPrefixRegex = "\\b({UpcomingPrefixRegex}?pr[oó]xim[oa]s?|siguiente|que\\s+viene)\\b" + .replace("{UpcomingPrefixRegex}", UpcomingPrefixRegex); + + public static final String PastPrefixRegex = "((este\\s+))"; + + public static final String PreviousPrefixRegex = "\\b({PastPrefixRegex}?pasad[oa](?!(\\s+el)?\\s+medio\\s*d[ií]a)|[uú]ltim[oa]|anterior)\\b" + .replace("{PastPrefixRegex}", PastPrefixRegex); + + public static final String ThisPrefixRegex = "(est?[ea]|actual)\\b"; + + public static final String PrefixWeekDayRegex = "(\\s*((,?\\s*el)|[-—–]))"; + + public static final String ThisRegex = "\\b((est[ae]\\s*)(semana{PrefixWeekDayRegex}?)?\\s*{WeekDayRegex})|({WeekDayRegex}\\s*((de\\s+)?esta\\s+semana))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{PrefixWeekDayRegex}", PrefixWeekDayRegex); + + public static final String LastDateRegex = "\\b(({PreviousPrefixRegex}\\s+(semana{PrefixWeekDayRegex}?)?|(la\\s+)?semana\\s+{PreviousPrefixRegex}{PrefixWeekDayRegex})\\s*{WeekDayRegex})|(este\\s+)?({WeekDayRegex}\\s+([uú]ltimo|pasado|anterior))|({WeekDayRegex}(\\s+((de\\s+)?((esta|la)\\s+([uú]ltima\\s+)?semana)|(de\\s+)?(la\\s+)?semana\\s+(pasada|anterior))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{PreviousPrefixRegex}", PreviousPrefixRegex) + .replace("{PrefixWeekDayRegex}", PrefixWeekDayRegex); + + public static final String NextDateRegex = "\\b((({NextPrefixRegex}\\s+)(semana{PrefixWeekDayRegex}?)?|(la\\s+)?semana\\s+{NextPrefixRegex}{PrefixWeekDayRegex})\\s*{WeekDayRegex})|(este\\s+)?({WeekDayRegex}\\s+(pr[oó]ximo|siguiente|que\\s+viene))|({WeekDayRegex}(\\s+(de\\s+)?(la\\s+)?((pr[oó]xima|siguiente)\\s+semana|semana\\s+(pr[oó]xima|siguiente))))\\b" + .replace("{WeekDayRegex}", WeekDayRegex) + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{PrefixWeekDayRegex}", PrefixWeekDayRegex); + + public static final String RelativeDayRegex = "(?((este|pr[oó]ximo|([uú]ltim(o|as|os)))\\s+días)|(días\\s+((que\\s+viene)|pasado)))\\b"; + + public static final String RestOfDateRegex = "\\bresto\\s+((del|de)\\s+)?((la|el|est?[ae])\\s+)?(?semana|mes|año|decada)(\\s+actual)?\\b"; + + public static final String DurationUnitRegex = "(?{DateUnitRegex}|horas?|hra?s?|hs?|minutos?|mins?|segundos?|segs?|noches?)\\b" + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String DurationConnectorRegex = "^[.]"; + + public static final String RelativeDurationUnitRegex = "(?:(?<=({NextPrefixRegex}|{PreviousPrefixRegex}|{ThisPrefixRegex})\\s+)({DurationUnitRegex}))" + .replace("{NextPrefixRegex}", NextPrefixRegex) + .replace("{PreviousPrefixRegex}", PreviousPrefixRegex) + .replace("{ThisPrefixRegex}", ThisPrefixRegex) + .replace("{DurationUnitRegex}", DurationUnitRegex); + + public static final String ReferencePrefixRegex = "(mism[ao]|aquel|est?e)\\b"; + + public static final String ReferenceDatePeriodRegex = "\\b{ReferencePrefixRegex}\\s+({DateUnitRegex}|fin\\s+de\\s+semana)\\b" + .replace("{ReferencePrefixRegex}", ReferencePrefixRegex) + .replace("{DateUnitRegex}", DateUnitRegex); + + public static final String FromToRegex = "\\b(from).+(to)\\b.+"; + + public static final String SingleAmbiguousMonthRegex = "^(the\\s+)?(may|march)$"; + + public static final String UnspecificDatePeriodRegex = "^[\\.]"; + + public static final String PrepositionSuffixRegex = "\\b(en|el|la|cerca|alrededor|desde|durante|hasta|hacia)$"; + + public static final String RestOfDateTimeRegex = "\\bresto\\s+((del?)\\s+)?((la|el|est[ae])\\s+)?(?(día|jornada))(\\s+de\\s+hoy)?\\b"; + + public static final String SetWeekDayRegex = "^[\\.]"; + + public static final String NightRegex = "\\b(medionoche|noche)\\b"; + + public static final String CommonDatePrefixRegex = "^[\\.]"; + + public static final String SuffixAfterRegex = "\\b((a\\s+)?(o|y)\\s+(arriba|despu[eé]s|posterior|mayor|m[aá]s\\s+tarde)(?!\\s+(que|de)))\\b"; + + public static final String YearPeriodRegex = "((((de(sde)?|durante|en)\\s+)?{YearRegex}\\s*({TillRegex})\\s*{YearRegex})|(((entre)\\s+){YearRegex}\\s*({RangeConnectorRegex})\\s*{YearRegex}))" + .replace("{YearRegex}", YearRegex) + .replace("{TillRegex}", TillRegex) + .replace("{RangeConnectorRegex}", RangeConnectorRegex); + + public static final String FutureSuffixRegex = "\\b(siguiente(s)?|pr[oó]xim[oa](s)?|(en\\s+el\\s+)?futuro|a\\s+partir\\s+de\\s+ahora)\\b"; + + public static final ImmutableMap WrittenDecades = ImmutableMap.builder() + .put("", 0) + .build(); + + public static final ImmutableMap SpecialDecadeCases = ImmutableMap.builder() + .put("", 0) + .build(); + + public static final String DefaultLanguageFallback = "DMY"; + + public static final List DurationDateRestrictions = Arrays.asList("hoy"); + + public static final ImmutableMap AmbiguityFiltersDict = ImmutableMap.builder() + .put("^mi$", "\\bmi\\b") + .put("^a[nñ]o$", "(? EarlyMorningTermList = Arrays.asList("madrugada"); + + public static final List MorningTermList = Arrays.asList("mañana", "la mañana"); + + public static final List AfternoonTermList = Arrays.asList("pasado mediodia", "pasado el mediodia", "pasado mediodía", "pasado el mediodía", "pasado medio dia", "pasado el medio dia", "pasado medio día", "pasado el medio día"); + + public static final List EveningTermList = Arrays.asList("tarde"); + + public static final List NightTermList = Arrays.asList("noche"); + + public static final List SameDayTerms = Arrays.asList("hoy", "el dia"); + + public static final List PlusOneDayTerms = Arrays.asList("mañana", "dia siguiente", "el dia de mañana", "proximo dia"); + + public static final List MinusOneDayTerms = Arrays.asList("ayer", "ultimo dia", "dia anterior"); + + public static final List PlusTwoDayTerms = Arrays.asList("pasado mañana", "dia despues de mañana"); + + public static final List MinusTwoDayTerms = Arrays.asList("anteayer", "dia antes de ayer"); + + public static final List MonthTerms = Arrays.asList("mes", "meses"); + + public static final List MonthToDateTerms = Arrays.asList("mes a la fecha", "meses a la fecha"); + + public static final List WeekendTerms = Arrays.asList("finde", "fin de semana", "fines de semana"); + + public static final List WeekTerms = Arrays.asList("semana"); + + public static final List YearTerms = Arrays.asList("año", "años"); + + public static final List YearToDateTerms = Arrays.asList("año a la fecha", "años a la fecha"); + + public static final ImmutableMap SpecialCharactersEquivalent = ImmutableMap.builder() + .put('á', 'a') + .put('é', 'e') + .put('í', 'i') + .put('ó', 'o') + .put('ú', 'u') + .build(); + + public static final String DoubleMultiplierRegex = "^(bi)(-|\\s)?"; + + public static final String DayTypeRegex = "(d[ií]as?|diari(o|as|amente))$"; + + public static final String WeekTypeRegex = "(semanas?|semanalmente)$"; + + public static final String BiWeekTypeRegex = "(quincenalmente)$"; + + public static final String WeekendTypeRegex = "(fin(es)?\\s+de\\s+semana|finde)$"; + + public static final String MonthTypeRegex = "(mes(es)?|mensual(es|mente)?)$"; + + public static final String YearTypeRegex = "(años?|anualmente)$"; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateExtractorConfiguration.java new file mode 100644 index 000000000..e8735c5bb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateExtractorConfiguration.java @@ -0,0 +1,245 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.utilities.SpanishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.number.spanish.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.number.spanish.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.spanish.parsers.SpanishNumberParserConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + + +public class SpanishDateExtractorConfiguration extends BaseOptionsConfiguration implements IDateExtractorConfiguration { + + public static final Pattern MonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthRegex); + public static final Pattern DayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DayRegex); + public static final Pattern MonthNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthNumRegex); + public static final Pattern YearRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.YearRegex); + public static final Pattern WeekDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekDayRegex); + public static final Pattern OnRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.OnRegex); + public static final Pattern RelaxedOnRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelaxedOnRegex); + public static final Pattern ThisRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ThisRegex); + public static final Pattern LastDateRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LastDateRegex); + public static final Pattern NextDateRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NextDateRegex); + public static final Pattern SpecialDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecialDayRegex); + public static final Pattern SpecialDayWithNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecialDayWithNumRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DateUnitRegex); + public static final Pattern WeekDayOfMonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekDayOfMonthRegex); + public static final Pattern SpecialDateRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecialDateRegex); + public static final Pattern RelativeWeekDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeWeekDayRegex); + public static final Pattern ForTheRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ForTheRegex); + public static final Pattern WeekDayAndDayOfMonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekDayAndDayOfMonthRegex); + public static final Pattern RelativeMonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeMonthRegex); + public static final Pattern StrictRelativeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.StrictRelativeRegex); + public static final Pattern PrefixArticleRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PrefixArticleRegex); + public static final Pattern RangeConnectorSymbolRegex = RegExpUtility.getSafeRegExp(BaseDateTime.RangeConnectorSymbolRegex); + public static final List ImplicitDateList = new ArrayList() { + { + add(OnRegex); + add(RelaxedOnRegex); + add(SpecialDayRegex); + add(ThisRegex); + add(LastDateRegex); + add(NextDateRegex); + add(WeekDayRegex); + add(WeekDayOfMonthRegex); + add(SpecialDateRegex); + } + }; + + public static final Pattern OfMonth = RegExpUtility.getSafeRegExp(SpanishDateTime.OfMonthRegex); + public static final Pattern MonthEnd = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthEndRegex); + public static final Pattern WeekDayEnd = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekDayEnd); + public static final Pattern YearSuffix = RegExpUtility.getSafeRegExp(SpanishDateTime.YearSuffix); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LessThanRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MoreThanRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.InConnectorRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangeUnitRegex); + public static final ImmutableMap DayOfWeek = SpanishDateTime.DayOfWeek; + public static final ImmutableMap MonthOfYear = SpanishDateTime.MonthOfYear; + + public static List DateRegexList; + + public SpanishDateExtractorConfiguration(IOptionsConfiguration config) { + super(config.getOptions()); + integerExtractor = new IntegerExtractor(); // in other languages (english) has a method named get instance + ordinalExtractor = new OrdinalExtractor(); // in other languages (english) has a method named get instance + numberParser = new BaseNumberParser(new SpanishNumberParserConfiguration()); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration()); + utilityConfiguration = new SpanishDatetimeUtilityConfiguration(); + + DateRegexList = new ArrayList() { + { + add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor1)); + add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor2)); + add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor3)); + } + }; + + boolean enableDmy = getDmyDateFormat() || SpanishDateTime.DefaultLanguageFallback == Constants.DefaultLanguageFallback_DMY; + + if (enableDmy) { + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor5)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor8)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor9)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor4)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor6)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor7)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor10)); + } else { + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor4)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor6)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor7)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor5)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor8)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor9)); + DateRegexList.add(RegExpUtility.getSafeRegExp(SpanishDateTime.DateExtractor10)); + } + } + + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + @Override + public Pattern getOfMonth() { + return OfMonth; + } + + @Override + public Pattern getMonthEnd() { + return MonthEnd; + } + + @Override + public Pattern getWeekDayEnd() { + return WeekDayEnd; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getForTheRegex() { + return ForTheRegex; + } + + @Override + public Pattern getWeekDayAndDayOfMonthRegex() { + return WeekDayAndDayOfMonthRegex; + } + + @Override + public Pattern getRelativeMonthRegex() { + return RelativeMonthRegex; + } + + @Override + public Pattern getStrictRelativeRegex() { + return StrictRelativeRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return WeekDayRegex; + } + + @Override + public Pattern getPrefixArticleRegex() { + return PrefixArticleRegex; + } + + @Override + public Pattern getYearSuffix() { + return YearSuffix; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getRangeConnectorSymbolRegex() { + return RangeConnectorSymbolRegex; + } + + @Override + public Iterable getDateRegexList() { + return DateRegexList; + } + + @Override + public Iterable getImplicitDateList() { + return ImplicitDateList; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public ImmutableMap getDayOfWeek() { + return DayOfWeek; + } + + @Override + public ImmutableMap getMonthOfYear() { + return MonthOfYear; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDatePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDatePeriodExtractorConfiguration.java new file mode 100644 index 000000000..9af664696 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDatePeriodExtractorConfiguration.java @@ -0,0 +1,320 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.BaseDateTime; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.number.spanish.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.number.spanish.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.spanish.parsers.SpanishNumberParserConfiguration; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpanishDatePeriodExtractorConfiguration extends BaseOptionsConfiguration implements IDatePeriodExtractorConfiguration { + public static final Pattern TillRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TillRegex); + public static final Pattern RangeConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangeConnectorRegex); + public static final Pattern DayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DayRegex); + public static final Pattern MonthNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthNumRegex); + public static final Pattern IllegalYearRegex = RegExpUtility.getSafeRegExp(BaseDateTime.IllegalYearRegex); + public static final Pattern YearRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.YearRegex); + public static final Pattern RelativeMonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeMonthRegex); + public static final Pattern MonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthRegex); + public static final Pattern MonthSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthSuffixRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DateUnitRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeUnitRegex); + public static final Pattern PastRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PastRegex); + public static final Pattern FutureRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.FutureRegex); + public static final Pattern FutureSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.FutureSuffixRegex); + + // composite regexes + public static final Pattern SimpleCasesRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SimpleCasesRegex); + public static final Pattern MonthFrontSimpleCasesRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthFrontSimpleCasesRegex); + public static final Pattern MonthFrontBetweenRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthFrontBetweenRegex); + public static final Pattern DayBetweenRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DayBetweenRegex); + public static final Pattern OneWordPeriodRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.OneWordPeriodRegex); + public static final Pattern MonthWithYearRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthWithYearRegex); + public static final Pattern MonthNumWithYearRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthNumWithYearRegex); + public static final Pattern WeekOfMonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekOfMonthRegex); + public static final Pattern WeekOfYearRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekOfYearRegex); + public static final Pattern FollowedDateUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.FollowedDateUnit); + public static final Pattern NumberCombinedWithDateUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.NumberCombinedWithDateUnit); + public static final Pattern QuarterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.QuarterRegex); + public static final Pattern QuarterRegexYearFront = RegExpUtility.getSafeRegExp(SpanishDateTime.QuarterRegexYearFront); + public static final Pattern AllHalfYearRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AllHalfYearRegex); + public static final Pattern SeasonRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SeasonRegex); + public static final Pattern WhichWeekRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WhichWeekRegex); + public static final Pattern WeekOfRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekOfRegex); + public static final Pattern MonthOfRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MonthOfRegex); + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangeUnitRegex); + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.InConnectorRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WithinNextPrefixRegex); + public static final Pattern LaterEarlyPeriodRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LaterEarlyPeriodRegex); + public static final Pattern RestOfDateRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RestOfDateRegex); + public static final Pattern WeekWithWeekDayRangeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekWithWeekDayRangeRegex); + public static final Pattern YearPlusNumberRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.YearPlusNumberRegex); + public static final Pattern DecadeWithCenturyRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DecadeWithCenturyRegex); + public static final Pattern YearPeriodRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.YearPeriodRegex); + public static final Pattern ComplexDatePeriodRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ComplexDatePeriodRegex); + public static final Pattern RelativeDecadeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeDecadeRegex); + public static final Pattern ReferenceDatePeriodRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ReferenceDatePeriodRegex); + public static final Pattern AgoRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AgoRegex); + public static final Pattern LaterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LaterRegex); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LessThanRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MoreThanRegex); + public static final Pattern CenturySuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.CenturySuffixRegex); + public static final Pattern NowRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NowRegex); + + public static final Iterable SimpleCasesRegexes = new ArrayList() { + { + add(SimpleCasesRegex); + add(DayBetweenRegex); + add(OneWordPeriodRegex); + add(MonthWithYearRegex); + add(MonthNumWithYearRegex); + add(YearRegex); + add(YearPeriodRegex); + add(WeekOfMonthRegex); + add(WeekOfYearRegex); + add(MonthFrontBetweenRegex); + add(MonthFrontSimpleCasesRegex); + add(QuarterRegex); + add(QuarterRegexYearFront); + add(SeasonRegex); + add(RestOfDateRegex); + add(LaterEarlyPeriodRegex); + add(WeekWithWeekDayRangeRegex); + add(YearPlusNumberRegex); + add(DecadeWithCenturyRegex); + add(RelativeDecadeRegex); + add(MonthOfRegex); + } + }; + + private static final Pattern fromRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.FromRegex); + private static final Pattern betweenRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.BetweenRegex); + + private final IDateTimeExtractor datePointExtractor; + private final IExtractor cardinalExtractor; + private final IExtractor ordinalExtractor; + private final IDateTimeExtractor durationExtractor; + private final IParser numberParser; + private final String[] durationDateRestrictions; + + public SpanishDatePeriodExtractorConfiguration(IOptionsConfiguration config) { + super(config.getOptions()); + + datePointExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + cardinalExtractor = CardinalExtractor.getInstance(); + ordinalExtractor = OrdinalExtractor.getInstance(); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration()); + numberParser = new BaseNumberParser(new SpanishNumberParserConfiguration()); + + durationDateRestrictions = SpanishDateTime.DurationDateRestrictions.toArray(new String[0]); + } + + @Override + public Iterable getSimpleCasesRegexes() { + return SimpleCasesRegexes; + } + + @Override + public Pattern getIllegalYearRegex() { + return IllegalYearRegex; + } + + @Override + public Pattern getYearRegex() { + return YearRegex; + } + + @Override + public Pattern getTillRegex() { + return TillRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getFollowedDateUnit() { + return FollowedDateUnit; + } + + @Override + public Pattern getNumberCombinedWithDateUnit() { + return NumberCombinedWithDateUnit; + } + + @Override + public Pattern getPastRegex() { + return PastRegex; + } + + @Override + public Pattern getFutureRegex() { + return FutureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return FutureSuffixRegex; + } + + @Override + public Pattern getWeekOfRegex() { + return WeekOfRegex; + } + + @Override + public Pattern getMonthOfRegex() { + return MonthOfRegex; + } + + @Override + public Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getYearPeriodRegex() { + return YearPeriodRegex; + } + + @Override + public Pattern getRelativeDecadeRegex() { + return RelativeDecadeRegex; + } + + @Override + public Pattern getReferenceDatePeriodRegex() { + return ReferenceDatePeriodRegex; + } + + @Override + public Pattern getAgoRegex() { + return AgoRegex; + } + + @Override + public Pattern getLaterRegex() { + return LaterRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public Pattern getCenturySuffixRegex() { + return CenturySuffixRegex; + } + + @Override + public Pattern getNowRegex() { + return NowRegex; + } + + @Override + public String[] getDurationDateRestrictions() { + return durationDateRestrictions; + } + + @Override + public ResultIndex getFromTokenIndex(String text) { + int index = -1; + boolean result = false; + Matcher matcher = fromRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(String text) { + int index = -1; + boolean result = false; + Matcher matcher = betweenRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(String text) { + Optional match = Arrays.stream(RegExpUtility.getMatches(RangeConnectorRegex, text)).findFirst(); + return match.isPresent() && match.get().length == text.trim().length(); + } + + @Override + public Pattern getComplexDatePeriodRegex() { + return ComplexDatePeriodRegex; + } + + @Override + public IDateTimeExtractor getDatePointExtractor() { + return datePointExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimeAltExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimeAltExtractorConfiguration.java new file mode 100644 index 000000000..36ba13b3c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimeAltExtractorConfiguration.java @@ -0,0 +1,90 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.config.IOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeAltExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class SpanishDateTimeAltExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimeAltExtractorConfiguration { + + private static final Pattern OrRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.OrRegex); + private static final Pattern DayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DayRegex); + + public static final Pattern ThisPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ThisPrefixRegex); + public static final Pattern PreviousPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PreviousPrefixRegex); + public static final Pattern NextPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NextPrefixRegex); + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AmRegex); + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PmRegex); + public static final Pattern RangePrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangePrefixRegex); + + public static final Iterable RelativePrefixList = new ArrayList() { + { + add(ThisPrefixRegex); + add(PreviousPrefixRegex); + add(NextPrefixRegex); + } + }; + + public static final Iterable AmPmRegexList = new ArrayList() { + { + add(AmRegex); + add(PmRegex); + } + }; + + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor datePeriodExtractor; + + public SpanishDateTimeAltExtractorConfiguration(IOptionsConfiguration config) { + super(config.getOptions()); + dateExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + datePeriodExtractor = new BaseDatePeriodExtractor(new SpanishDatePeriodExtractorConfiguration(this)); + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + @Override + public Iterable getRelativePrefixList() { + return RelativePrefixList; + } + + @Override + public Iterable getAmPmRegexList() { + return AmPmRegexList; + } + + @Override + public Pattern getOrRegex() { + return OrRegex; + } + + @Override + public Pattern getThisPrefixRegex() { + return ThisPrefixRegex; + } + + @Override + public Pattern getDayRegex() { + return DayRegex; + } + + @Override public Pattern getRangePrefixRegex() { + return RangePrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimeExtractorConfiguration.java new file mode 100644 index 000000000..f0fda81a8 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimeExtractorConfiguration.java @@ -0,0 +1,170 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.utilities.SpanishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.english.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.Arrays; +import java.util.regex.Pattern; + +public class SpanishDateTimeExtractorConfiguration extends BaseOptionsConfiguration implements IDateTimeExtractorConfiguration { + + public static final Pattern PrepositionRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PrepositionRegex); + public static final Pattern NowRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NowRegex); + public static final Pattern SuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SuffixRegex); + + //TODO: modify it according to the corresponding English regex + + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeOfDayRegex); + public static final Pattern SpecificTimeOfDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecificTimeOfDayRegex); + public static final Pattern TimeOfTodayAfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeOfTodayAfterRegex); + public static final Pattern TimeOfTodayBeforeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeOfTodayBeforeRegex); + public static final Pattern SimpleTimeOfTodayAfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SimpleTimeOfTodayAfterRegex); + public static final Pattern SimpleTimeOfTodayBeforeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SimpleTimeOfTodayBeforeRegex); + public static final Pattern SpecificEndOfRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecificEndOfRegex); + public static final Pattern UnspecificEndOfRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.UnspecificEndOfRegex); + + //TODO: add this for Spanish + public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeUnitRegex); + public static final Pattern ConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ConnectorRegex); + public static final Pattern NumberAsTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NumberAsTimeRegex); + public static final Pattern DateNumberConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DateNumberConnectorRegex); + public static final Pattern SuffixAfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SuffixAfterRegex); + + public SpanishDateTimeExtractorConfiguration(DateTimeOptions options) { + + super(options); + + integerExtractor = IntegerExtractor.getInstance(); + datePointExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + timePointExtractor = new BaseTimeExtractor(new SpanishTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration(options)); + + utilityConfiguration = new SpanishDatetimeUtilityConfiguration(); + } + + public SpanishDateTimeExtractorConfiguration() { + this(DateTimeOptions.None); + } + + private IExtractor integerExtractor; + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + private IDateExtractor datePointExtractor; + + @Override + public IDateExtractor getDatePointExtractor() { + return datePointExtractor; + } + + private IDateTimeExtractor timePointExtractor; + + @Override + public IDateTimeExtractor getTimePointExtractor() { + return timePointExtractor; + } + + private IDateTimeExtractor durationExtractor; + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeUtilityConfiguration utilityConfiguration; + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public Pattern getNowRegex() { + return NowRegex; + } + + @Override + public Pattern getSuffixRegex() { + return SuffixRegex; + } + + @Override + public Pattern getTimeOfTodayAfterRegex() { + return TimeOfTodayAfterRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayAfterRegex() { + return SimpleTimeOfTodayAfterRegex; + } + + @Override + public Pattern getTimeOfTodayBeforeRegex() { + return TimeOfTodayBeforeRegex; + } + + @Override + public Pattern getSimpleTimeOfTodayBeforeRegex() { + return SimpleTimeOfTodayBeforeRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return TimeOfDayRegex; + } + + @Override + public Pattern getSpecificEndOfRegex() { + return SpecificEndOfRegex; + } + + @Override + public Pattern getUnspecificEndOfRegex() { + return UnspecificEndOfRegex; + } + + @Override + public Pattern getUnitRegex() { + return UnitRegex; + } + + @Override + public Pattern getNumberAsTimeRegex() { + return NumberAsTimeRegex; + } + + @Override + public Pattern getDateNumberConnectorRegex() { + return DateNumberConnectorRegex; + } + + @Override + public Pattern getSuffixAfterRegex() { + return SuffixAfterRegex; + } + + public boolean isConnector(String text) { + + text = text.trim(); + + boolean isPreposition = Arrays.stream(RegExpUtility.getMatches(PrepositionRegex, text)).findFirst().isPresent(); + boolean isConnector = Arrays.stream(RegExpUtility.getMatches(ConnectorRegex, text)).findFirst().isPresent(); + return (StringUtility.isNullOrEmpty(text) || isPreposition || isConnector); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..8ba69bc8b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDateTimePeriodExtractorConfiguration.java @@ -0,0 +1,283 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.number.spanish.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpanishDateTimePeriodExtractorConfiguration extends BaseOptionsConfiguration + implements IDateTimePeriodExtractorConfiguration { + + public static final Pattern weekDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WeekDayRegex); + public static final Pattern NumberCombinedWithUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.DateTimePeriodNumberCombinedWithUnit); + public static final Pattern RestOfDateTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RestOfDateTimeRegex); + public static final Pattern PeriodTimeOfDayWithDateRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PeriodTimeOfDayWithDateRegex); + public static final Pattern RelativeTimeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeTimeUnitRegex); + public static final Pattern GeneralEndingRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.GeneralEndingRegex); + public static final Pattern MiddlePauseRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MiddlePauseRegex); + public static final Pattern AmDescRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AmDescRegex); + public static final Pattern PmDescRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PmDescRegex); + public static final Pattern WithinNextPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WithinNextPrefixRegex); + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DateUnitRegex); + public static final Pattern PrefixDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PrefixDayRegex); + public static final Pattern SuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SuffixRegex); + public static final Pattern BeforeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.BeforeRegex); + public static final Pattern AfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AfterRegex); + public static final Pattern FromRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.FromRegex); + public static final Pattern RangeConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangeConnectorRegex); + public static final Pattern BetweenRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.BetweenRegex); + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeOfDayRegex); + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeUnitRegex); + public static final Pattern TimeFollowedUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeFollowedUnit); + + private final String tokenBeforeDate; + + private final IExtractor cardinalExtractor; + private final IDateTimeExtractor singleDateExtractor; + private final IDateTimeExtractor singleTimeExtractor; + private final IDateTimeExtractor singleDateTimeExtractor; + private final IDateTimeExtractor durationExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor timeZoneExtractor; + + public static final Iterable SimpleCases = new ArrayList() { + { + add(SpanishTimePeriodExtractorConfiguration.PureNumFromTo); + add(SpanishTimePeriodExtractorConfiguration.PureNumBetweenAnd); + } + }; + + public SpanishDateTimePeriodExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public SpanishDateTimePeriodExtractorConfiguration(DateTimeOptions options) { + + super(options); + tokenBeforeDate = SpanishDateTime.TokenBeforeDate; + + cardinalExtractor = CardinalExtractor.getInstance(); + + singleDateExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + singleTimeExtractor = new BaseTimeExtractor(new SpanishTimeExtractorConfiguration(options)); + singleDateTimeExtractor = new BaseDateTimeExtractor(new SpanishDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration(options)); + timePeriodExtractor = new BaseTimePeriodExtractor(new SpanishTimePeriodExtractorConfiguration(options)); + timeZoneExtractor = new BaseTimeZoneExtractor(new SpanishTimeZoneExtractorConfiguration(options)); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IDateTimeExtractor getSingleDateExtractor() { + return singleDateExtractor; + } + + @Override + public IDateTimeExtractor getSingleTimeExtractor() { + return singleTimeExtractor; + } + + @Override + public IDateTimeExtractor getSingleDateTimeExtractor() { + return singleDateTimeExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + @Override + public Iterable getSimpleCasesRegex() { + return SimpleCases; + } + + @Override + public Pattern getPrepositionRegex() { + return SpanishDateTimeExtractorConfiguration.PrepositionRegex; + } + + @Override + public Pattern getTillRegex() { + return SpanishTimePeriodExtractorConfiguration.TillRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return SpanishDateTimeExtractorConfiguration.TimeOfDayRegex; + } + + @Override + public Pattern getFollowedUnit() { + return TimeFollowedUnit; + } + + @Override + public Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return SpanishDatePeriodExtractorConfiguration.PastRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return SpanishDatePeriodExtractorConfiguration.FutureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return SpanishDatePeriodExtractorConfiguration.FutureSuffixRegex; + } + + @Override + public Pattern getPrefixDayRegex() { + return PrefixDayRegex; + } + + @Override + public Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return NumberCombinedWithUnit; + } + + @Override + public Pattern getWeekDayRegex() { + return weekDayRegex; + } + + @Override + public Pattern getPeriodTimeOfDayWithDateRegex() { + return PeriodTimeOfDayWithDateRegex; + } + + @Override + public Pattern getRelativeTimeUnitRegex() { + return RelativeTimeUnitRegex; + } + + @Override + public Pattern getRestOfDateTimeRegex() { + return RestOfDateTimeRegex; + } + + @Override + public Pattern getGeneralEndingRegex() { + return GeneralEndingRegex; + } + + @Override + public Pattern getMiddlePauseRegex() { + return MiddlePauseRegex; + } + + @Override + public Pattern getAmDescRegex() { + return AmDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return PmDescRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public Pattern getSuffixRegex() { + return SuffixRegex; + } + + @Override + public Pattern getBeforeRegex() { + return BeforeRegex; + } + + @Override + public Pattern getAfterRegex() { + return AfterRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return SpanishDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + } + + @Override + public ResultIndex getFromTokenIndex(String text) { + int index = -1; + boolean result = false; + Matcher matcher = FromRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(String text) { + int index = -1; + boolean result = false; + Matcher matcher = BetweenRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(String text) { + Optional match = Arrays.stream(RegExpUtility.getMatches(RangeConnectorRegex, text)).findFirst(); + return match.isPresent() && match.get().length == text.trim().length(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDurationExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDurationExtractorConfiguration.java new file mode 100644 index 000000000..064ff06dc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishDurationExtractorConfiguration.java @@ -0,0 +1,139 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.IDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.number.spanish.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class SpanishDurationExtractorConfiguration extends BaseOptionsConfiguration implements IDurationExtractorConfiguration { + + //public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.UnitRegex); + public static final Pattern SuffixAndRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SuffixAndRegex); + public static final Pattern FollowedUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.FollowedUnit); + public static final Pattern NumberCombinedWithUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.DurationNumberCombinedWithUnit); + public static final Pattern AnUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AnUnitRegex); + public static final Pattern DuringRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DuringRegex); + public static final Pattern AllRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AllRegex); + public static final Pattern HalfRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.HalfRegex); + public static final Pattern ConjunctionRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ConjunctionRegex); + public static final Pattern InexactNumberRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.InexactNumberRegex); + public static final Pattern InexactNumberUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.InexactNumberUnitRegex); + public static final Pattern RelativeDurationUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeDurationUnitRegex); + public static final Pattern DurationUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DurationUnitRegex); + public static final Pattern DurationConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DurationConnectorRegex); + public static final Pattern MoreThanRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MoreThanRegex); + public static final Pattern LessThanRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LessThanRegex); + + private final IExtractor cardinalExtractor; + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + + public SpanishDurationExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public SpanishDurationExtractorConfiguration(DateTimeOptions options) { + + super(options); + + cardinalExtractor = CardinalExtractor.getInstance(); + unitMap = SpanishDateTime.UnitMap; + unitValueMap = SpanishDateTime.UnitValueMap; + } + + @Override + public Pattern getFollowedUnit() { + return FollowedUnit; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return NumberCombinedWithUnit; + } + + @Override + public Pattern getAnUnitRegex() { + return AnUnitRegex; + } + + @Override + public Pattern getDuringRegex() { + return DuringRegex; + } + + @Override + public Pattern getAllRegex() { + return AllRegex; + } + + @Override + public Pattern getHalfRegex() { + return HalfRegex; + } + + @Override + public Pattern getSuffixAndRegex() { + return SuffixAndRegex; + } + + @Override + public Pattern getConjunctionRegex() { + return ConjunctionRegex; + } + + @Override + public Pattern getInexactNumberRegex() { + return InexactNumberRegex; + } + + @Override + public Pattern getInexactNumberUnitRegex() { + return InexactNumberUnitRegex; + } + + @Override + public Pattern getRelativeDurationUnitRegex() { + return RelativeDurationUnitRegex; + } + + @Override + public Pattern getDurationUnitRegex() { + return DurationUnitRegex; + } + + @Override + public Pattern getDurationConnectorRegex() { + return DurationConnectorRegex; + } + + @Override + public Pattern getLessThanRegex() { + return LessThanRegex; + } + + @Override + public Pattern getMoreThanRegex() { + return MoreThanRegex; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishHolidayExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishHolidayExtractorConfiguration.java new file mode 100644 index 000000000..3a3e6fb8f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishHolidayExtractorConfiguration.java @@ -0,0 +1,36 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.IHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class SpanishHolidayExtractorConfiguration extends BaseOptionsConfiguration implements IHolidayExtractorConfiguration { + + public static final Pattern H1 = RegExpUtility.getSafeRegExp(SpanishDateTime.HolidayRegex1); + + public static final Pattern H2 = RegExpUtility.getSafeRegExp(SpanishDateTime.HolidayRegex2); + + public static final Pattern H3 = RegExpUtility.getSafeRegExp(SpanishDateTime.HolidayRegex3); + + public static final Iterable HolidayRegexList = new ArrayList() { + { + add(H1); + add(H2); + add(H3); + } + }; + + public SpanishHolidayExtractorConfiguration() { + super(DateTimeOptions.None); + } + + @Override + public Iterable getHolidayRegexes() { + return HolidayRegexList; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishMergedExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishMergedExtractorConfiguration.java new file mode 100644 index 000000000..db952e02b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishMergedExtractorConfiguration.java @@ -0,0 +1,198 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeAltExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseHolidayExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseSetExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeListExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.IMergedExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import com.microsoft.recognizers.text.number.spanish.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +import org.javatuples.Pair; + +public class SpanishMergedExtractorConfiguration extends BaseOptionsConfiguration implements IMergedExtractorConfiguration { + + public static final Pattern BeforeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.BeforeRegex); + public static final Pattern AfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AfterRegex); + public static final Pattern SinceRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SinceRegex); + public static final Pattern AroundRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AroundRegex); + public static final Pattern FromToRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.FromToRegex); + public static final Pattern SingleAmbiguousMonthRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SingleAmbiguousMonthRegex); + public static final Pattern PrepositionSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PrepositionSuffixRegex); + public static final Pattern AmbiguousRangeModifierPrefix = RegExpUtility.getSafeRegExp(SpanishDateTime.AmbiguousRangeModifierPrefix); + public static final Pattern NumberEndingPattern = RegExpUtility.getSafeRegExp(SpanishDateTime.NumberEndingPattern); + public static final Pattern SuffixAfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SuffixAfterRegex); + public static final Pattern UnspecificDatePeriodRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.UnspecificDatePeriodRegex); + public final Iterable> ambiguityFiltersDict = null; + + public static final StringMatcher SuperfluousWordMatcher = new StringMatcher(); + + public SpanishMergedExtractorConfiguration(DateTimeOptions options) { + super(options); + + setExtractor = new BaseSetExtractor(new SpanishSetExtractorConfiguration(options)); + dateExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + timeExtractor = new BaseTimeExtractor(new SpanishTimeExtractorConfiguration(options)); + holidayExtractor = new BaseHolidayExtractor(new SpanishHolidayExtractorConfiguration()); + datePeriodExtractor = new BaseDatePeriodExtractor(new SpanishDatePeriodExtractorConfiguration(this)); + dateTimeExtractor = new BaseDateTimeExtractor(new SpanishDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration(options)); + timeZoneExtractor = new BaseTimeZoneExtractor(new SpanishTimeZoneExtractorConfiguration(options)); + dateTimeAltExtractor = new BaseDateTimeAltExtractor(new SpanishDateTimeAltExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new SpanishTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor(new SpanishDateTimePeriodExtractorConfiguration(options)); + integerExtractor = IntegerExtractor.getInstance(); + } + + public final StringMatcher getSuperfluousWordMatcher() { + return SuperfluousWordMatcher; + } + + private IDateExtractor dateExtractor; + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + private IDateTimeExtractor timeExtractor; + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + private IDateTimeExtractor dateTimeExtractor; + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + private IDateTimeExtractor datePeriodExtractor; + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + private IDateTimeExtractor timePeriodExtractor; + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + private IDateTimeExtractor dateTimePeriodExtractor; + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor setExtractor; + + public final IDateTimeExtractor getSetExtractor() { + return setExtractor; + } + + private IDateTimeExtractor holidayExtractor; + + public final IDateTimeExtractor getHolidayExtractor() { + return holidayExtractor; + } + + private IDateTimeZoneExtractor timeZoneExtractor; + + public final IDateTimeZoneExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + private IDateTimeListExtractor dateTimeAltExtractor; + + public final IDateTimeListExtractor getDateTimeAltExtractor() { + return dateTimeAltExtractor; + } + + private IExtractor integerExtractor; + + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + public final Iterable> getAmbiguityFiltersDict() { + return ambiguityFiltersDict; + } + + @Override + public Iterable getFilterWordRegexList() { + return null; + } + + public final Pattern getAfterRegex() { + return AfterRegex; + } + + public final Pattern getBeforeRegex() { + return BeforeRegex; + } + + public final Pattern getSinceRegex() { + return SinceRegex; + } + + public final Pattern getAroundRegex() { + return AroundRegex; + } + + public final Pattern getFromToRegex() { + return FromToRegex; + } + + public final Pattern getSingleAmbiguousMonthRegex() { + return SingleAmbiguousMonthRegex; + } + + public final Pattern getPrepositionSuffixRegex() { + return PrepositionSuffixRegex; + } + + public final Pattern getAmbiguousRangeModifierPrefix() { + return null; + } + + public final Pattern getPotentialAmbiguousRangeRegex() { + return null; + } + + public final Pattern getNumberEndingPattern() { + return NumberEndingPattern; + } + + public final Pattern getSuffixAfterRegex() { + return SuffixAfterRegex; + } + + public final Pattern getUnspecificDatePeriodRegex() { + return UnspecificDatePeriodRegex; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishSetExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishSetExtractorConfiguration.java new file mode 100644 index 000000000..023ecb728 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishSetExtractorConfiguration.java @@ -0,0 +1,119 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ISetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class SpanishSetExtractorConfiguration extends BaseOptionsConfiguration implements ISetExtractorConfiguration { + + public static final Pattern PeriodicRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PeriodicRegex); + public static final Pattern EachUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.EachUnitRegex); + public static final Pattern EachPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.EachPrefixRegex); + public static final Pattern EachDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.EachDayRegex); + public static final Pattern BeforeEachDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.BeforeEachDayRegex); + public static final Pattern SetWeekDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SetWeekDayRegex); + public static final Pattern SetEachRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.SetEachRegex); + + public SpanishSetExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public SpanishSetExtractorConfiguration(DateTimeOptions options) { + super(options); + + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration()); + timeExtractor = new BaseTimeExtractor(new SpanishTimeExtractorConfiguration(options)); + dateExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + dateTimeExtractor = new BaseDateTimeExtractor(new SpanishDateTimeExtractorConfiguration(options)); + datePeriodExtractor = new BaseDatePeriodExtractor(new SpanishDatePeriodExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new SpanishTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor(new SpanishDateTimePeriodExtractorConfiguration(options)); + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor timeExtractor; + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + private IDateExtractor dateExtractor; + + public final IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + private IDateTimeExtractor dateTimeExtractor; + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + private IDateTimeExtractor datePeriodExtractor; + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + private IDateTimeExtractor timePeriodExtractor; + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + private IDateTimeExtractor dateTimePeriodExtractor; + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + public final Pattern getLastRegex() { + return SpanishDateExtractorConfiguration.LastDateRegex; + } + + public final Pattern getEachPrefixRegex() { + return EachPrefixRegex; + } + + public final Pattern getPeriodicRegex() { + return PeriodicRegex; + } + + public final Pattern getEachUnitRegex() { + return EachUnitRegex; + } + + public final Pattern getEachDayRegex() { + return EachDayRegex; + } + + public final Pattern getBeforeEachDayRegex() { + return BeforeEachDayRegex; + } + + public final Pattern getSetWeekDayRegex() { + return SetWeekDayRegex; + } + + public final Pattern getSetEachRegex() { + return SetEachRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimeExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimeExtractorConfiguration.java new file mode 100644 index 000000000..d92bb5e73 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimeExtractorConfiguration.java @@ -0,0 +1,132 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class SpanishTimeExtractorConfiguration extends BaseOptionsConfiguration + implements ITimeExtractorConfiguration { + + // part 1: smallest component + // -------------------------------------- + public static final Pattern DescRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DescRegex); + public static final Pattern HourNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.HourNumRegex); + public static final Pattern MinuteNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MinuteNumRegex); + + // part 2: middle level component + // -------------------------------------- + // handle "... en punto" + public static final Pattern OclockRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.OclockRegex); + + // handle "... tarde" + public static final Pattern PmRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PmRegex); + + // handle "... de la mañana" + public static final Pattern AmRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AmRegex); + + // handle "y media ..." "menos cuarto ..." + public static final Pattern LessThanOneHour = RegExpUtility.getSafeRegExp(SpanishDateTime.LessThanOneHour); + public static final Pattern TensTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TensTimeRegex); + + // handle "seis treinta", "seis veintiuno", "seis menos diez" + public static final Pattern WrittenTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WrittenTimeRegex); + public static final Pattern TimePrefix = RegExpUtility.getSafeRegExp(SpanishDateTime.TimePrefix); + public static final Pattern TimeSuffix = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeSuffix); + public static final Pattern BasicTime = RegExpUtility.getSafeRegExp(SpanishDateTime.BasicTime); + + // part 3: regex for time + // -------------------------------------- + // handle "a las cuatro" "a las 3" + //TODO: add some new regex which have used in AtRegex + //TODO: modify according to corresponding English regex + public static final Pattern AtRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AtRegex); + public static final Pattern ConnectNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ConnectNumRegex); + public static final Pattern TimeBeforeAfterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeBeforeAfterRegex); + public static final Iterable TimeRegexList = new ArrayList() { + { + // (tres min pasadas las)? siete|7|(siete treinta) pm + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex1)); + + // (tres min pasadas las)? 3:00(:00)? (pm)? + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex2)); + + // (tres min pasadas las)? 3.00 (pm) + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex3)); + + // (tres min pasadas las) (cinco treinta|siete|7|7:00(:00)?) (pm)? + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex4)); + + // (tres min pasadas las) (cinco treinta|siete|7|7:00(:00)?) (pm)? (de la noche) + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex5)); + + // (cinco treinta|siete|7|7:00(:00)?) (pm)? (de la noche) + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex6)); + + // (En la noche) a las (cinco treinta|siete|7|7:00(:00)?) (pm)? + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex7)); + + // (En la noche) (cinco treinta|siete|7|7:00(:00)?) (pm)? + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex8)); + + // once (y)? veinticinco + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex9)); + + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex10)); + + // (tres menos veinte) (pm)? + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex11)); + + // (tres min pasadas las)? 3h00 (pm)? + add(RegExpUtility.getSafeRegExp(SpanishDateTime.TimeRegex12)); + + // 340pm + add(ConnectNumRegex); + } + }; + + public final Pattern getIshRegex() { + return null; + } + + public final Iterable getTimeRegexList() { + return TimeRegexList; + } + + public final Pattern getAtRegex() { + return AtRegex; + } + + public final Pattern getTimeBeforeAfterRegex() { + return TimeBeforeAfterRegex; + } + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeExtractor timeZoneExtractor; + + public final IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + public SpanishTimeExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public SpanishTimeExtractorConfiguration(DateTimeOptions options) { + super(options); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration()); + timeZoneExtractor = new BaseTimeZoneExtractor(new SpanishTimeZoneExtractorConfiguration(options)); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimePeriodExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimePeriodExtractorConfiguration.java new file mode 100644 index 000000000..7a03533f6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimePeriodExtractorConfiguration.java @@ -0,0 +1,154 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeZoneExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultIndex; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.utilities.SpanishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.spanish.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpanishTimePeriodExtractorConfiguration extends BaseOptionsConfiguration implements ITimePeriodExtractorConfiguration { + + private String tokenBeforeDate; + + public final String getTokenBeforeDate() { + return tokenBeforeDate; + } + + public static final Pattern HourNumRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.HourNumRegex); + public static final Pattern PureNumFromTo = RegExpUtility.getSafeRegExp(SpanishDateTime.PureNumFromTo); + public static final Pattern PureNumBetweenAnd = RegExpUtility.getSafeRegExp(SpanishDateTime.PureNumBetweenAnd); + public static final Pattern SpecificTimeFromTo = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecificTimeFromTo); + public static final Pattern SpecificTimeBetweenAnd = RegExpUtility.getSafeRegExp(SpanishDateTime.SpecificTimeBetweenAnd); + public static final Pattern UnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.UnitRegex); + public static final Pattern FollowedUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.FollowedUnit); + public static final Pattern NumberCombinedWithUnit = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeNumberCombinedWithUnit); + + private static final Pattern FromRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.FromRegex); + private static final Pattern RangeConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangeConnectorRegex); + private static final Pattern BetweenRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.BetweenRegex); + + public static final Pattern TimeOfDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeOfDayRegex); + public static final Pattern GeneralEndingRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.GeneralEndingRegex); + public static final Pattern TillRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TillRegex); + + public SpanishTimePeriodExtractorConfiguration() { + this(DateTimeOptions.None); + } + + public SpanishTimePeriodExtractorConfiguration(DateTimeOptions options) { + + super(options); + + tokenBeforeDate = SpanishDateTime.TokenBeforeDate; + singleTimeExtractor = new BaseTimeExtractor(new SpanishTimeExtractorConfiguration(options)); + utilityConfiguration = new SpanishDatetimeUtilityConfiguration(); + integerExtractor = IntegerExtractor.getInstance(); + timeZoneExtractor = new BaseTimeZoneExtractor(new SpanishTimeZoneExtractorConfiguration(options)); + } + + private IDateTimeUtilityConfiguration utilityConfiguration; + + public final IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + private IDateTimeExtractor singleTimeExtractor; + + public final IDateTimeExtractor getSingleTimeExtractor() { + return singleTimeExtractor; + } + + private IExtractor integerExtractor; + + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + public final IDateTimeExtractor timeZoneExtractor; + + public IDateTimeExtractor getTimeZoneExtractor() { + return timeZoneExtractor; + } + + + public Iterable getSimpleCasesRegex() { + return getSimpleCasesRegex; + } + + public final Iterable getSimpleCasesRegex = new ArrayList() { + { + add(PureNumFromTo); + add(PureNumBetweenAnd); + } + }; + + public Iterable getPureNumberRegex() { + return getPureNumberRegex; + } + + public final Iterable getPureNumberRegex = new ArrayList() { + { + add(PureNumFromTo); + add(PureNumBetweenAnd); + } + }; + + public final Pattern getTillRegex() { + return TillRegex; + } + + public final Pattern getTimeOfDayRegex() { + return TimeOfDayRegex; + } + + public final Pattern getGeneralEndingRegex() { + return GeneralEndingRegex; + } + + @Override + public ResultIndex getFromTokenIndex(String text) { + int index = -1; + boolean result = false; + Matcher matcher = FromRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public ResultIndex getBetweenTokenIndex(String text) { + int index = -1; + boolean result = false; + Matcher matcher = BetweenRegex.matcher(text); + if (matcher.find()) { + result = true; + index = matcher.start(); + } + + return new ResultIndex(result, index); + } + + @Override + public boolean hasConnectorToken(String text) { + Optional match = Arrays.stream(RegExpUtility.getMatches(RangeConnectorRegex, text)).findFirst(); + return match.isPresent() && match.get().length == text.trim().length(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimeZoneExtractorConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimeZoneExtractorConfiguration.java new file mode 100644 index 000000000..09ede1427 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/extractors/SpanishTimeZoneExtractorConfiguration.java @@ -0,0 +1,44 @@ +package com.microsoft.recognizers.text.datetime.spanish.extractors; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.config.ITimeZoneExtractorConfiguration; +import com.microsoft.recognizers.text.matcher.StringMatcher; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class SpanishTimeZoneExtractorConfiguration extends BaseOptionsConfiguration implements ITimeZoneExtractorConfiguration { + public SpanishTimeZoneExtractorConfiguration(DateTimeOptions options) { + super(options); + + } + + private Pattern directUtcRegex; + + public final Pattern getDirectUtcRegex() { + return directUtcRegex; + } + + private Pattern locationTimeSuffixRegex; + + public final Pattern getLocationTimeSuffixRegex() { + return locationTimeSuffixRegex; + } + + private StringMatcher locationMatcher; + + public final StringMatcher getLocationMatcher() { + return locationMatcher; + } + + private StringMatcher timeZoneMatcher; + + public final StringMatcher getTimeZoneMatcher() { + return timeZoneMatcher; + } + + public final ArrayList getAmbiguousTimezoneList() { + return new ArrayList<>(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/DateTimePeriodParser.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/DateTimePeriodParser.java new file mode 100644 index 000000000..527a1e934 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/DateTimePeriodParser.java @@ -0,0 +1,133 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.DateTimeParseResult; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil; +import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import org.javatuples.Pair; + +public class DateTimePeriodParser extends BaseDateTimePeriodParser { + + public DateTimePeriodParser(IDateTimePeriodParserConfiguration configuration) { + + super(configuration); + } + + @Override + protected DateTimeResolutionResult parseSpecificTimeOfDay(String text, LocalDateTime referenceTime) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + + // handle morning, afternoon.. + MatchedTimeRangeResult matchedTimeRangeResult = this.config.getMatchedTimeRange(trimmedText, "-1", -1, -1, -1); + if (!matchedTimeRangeResult.getMatched()) { + return ret; + } + + boolean exactMatch = RegexExtension.isExactMatch(this.config.getSpecificTimeOfDayRegex(),trimmedText, true); + + if (exactMatch) { + int swift = this.config.getSwiftPrefix(trimmedText); + LocalDateTime date = referenceTime.plusDays(swift); + int day = date.getDayOfMonth(); + int month = date.getMonthValue(); + int year = date.getYear(); + + ret.setTimex(DateTimeFormatUtil.formatDate(date) + matchedTimeRangeResult.getTimeStr()); + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromValue( + date, year, month, day, matchedTimeRangeResult.getBeginHour(), 0, 0), + DateUtil.safeCreateFromValue( + date, year, month, day, matchedTimeRangeResult.getEndHour(), matchedTimeRangeResult.getEndMin(), matchedTimeRangeResult.getEndMin()))); + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromValue( + date, year, month, day, matchedTimeRangeResult.getBeginHour(), 0, 0), + DateUtil.safeCreateFromValue( + date, year, month, day, matchedTimeRangeResult.getEndHour(), matchedTimeRangeResult.getEndMin(), matchedTimeRangeResult.getEndMin()))); + ret.setSuccess(true); + + return ret; + } + + int startIndex = trimmedText.indexOf(SpanishDateTime.Tomorrow); + if (startIndex == 0) { + startIndex = SpanishDateTime.Tomorrow.length(); + } else { + startIndex = 0; + } + + // handle Date followed by morning, afternoon + // Add handling code to handle morning, afternoon followed by Date + // Add handling code to handle early/late morning, afternoon + Optional match = Arrays.stream(RegExpUtility.getMatches(this.config .getTimeOfDayRegex(), trimmedText.substring(startIndex))).findFirst(); + if (match.isPresent()) { + String beforeStr = trimmedText.substring(0, match.get().index).trim(); + List ers = this.config.getDateExtractor().extract(beforeStr, referenceTime); + + if (ers.size() == 0) { + return ret; + } + + DateTimeParseResult pr = this.config.getDateParser().parse(ers.get(0), referenceTime); + LocalDateTime futureDate = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getFutureValue(); + LocalDateTime pastDate = (LocalDateTime)((DateTimeResolutionResult)pr.getValue()).getPastValue(); + + ret.setTimex(pr.getTimexStr() + matchedTimeRangeResult.getTimeStr()); + + ret.setFutureValue(new Pair<>( + DateUtil.safeCreateFromValue( + futureDate, + futureDate.getYear(), + futureDate.getMonthValue(), + futureDate.getDayOfMonth(), + matchedTimeRangeResult.getBeginHour(), + 0, + 0), + DateUtil.safeCreateFromValue( + futureDate, + futureDate.getYear(), + futureDate.getMonthValue(), + futureDate.getDayOfMonth(), + matchedTimeRangeResult.getEndHour(), + matchedTimeRangeResult.getEndMin(), + matchedTimeRangeResult.getEndMin()))); + ret.setPastValue(new Pair<>( + DateUtil.safeCreateFromValue(pastDate, + pastDate.getYear(), + pastDate.getMonthValue(), + pastDate.getDayOfMonth(), + matchedTimeRangeResult.getBeginHour(), + 0, + 0), + DateUtil.safeCreateFromValue( + pastDate, + pastDate.getYear(), + pastDate.getMonthValue(), + pastDate.getDayOfMonth(), + matchedTimeRangeResult.getEndHour(), + matchedTimeRangeResult.getEndMin(), + matchedTimeRangeResult.getEndMin()))); + ret.setSuccess(true); + + return ret; + } + + return ret; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishCommonDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishCommonDateTimeParserConfiguration.java new file mode 100644 index 000000000..ec5a90061 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishCommonDateTimeParserConfiguration.java @@ -0,0 +1,287 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.english.parsers.TimeParser; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDatePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDateTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.BaseTimePeriodExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDatePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimeAltParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDateTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseDurationParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimePeriodParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.BaseDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDurationExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.utilities.SpanishDatetimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.number.parsers.BaseNumberParser; +import com.microsoft.recognizers.text.number.spanish.extractors.CardinalExtractor; +import com.microsoft.recognizers.text.number.spanish.extractors.IntegerExtractor; +import com.microsoft.recognizers.text.number.spanish.extractors.OrdinalExtractor; +import com.microsoft.recognizers.text.number.spanish.parsers.SpanishNumberParserConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class SpanishCommonDateTimeParserConfiguration extends BaseDateParserConfiguration implements ICommonDateTimeParserConfiguration { + + private final IDateTimeUtilityConfiguration utilityConfiguration; + + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + private final ImmutableMap seasonMap; + private final ImmutableMap specialYearPrefixesMap; + private final ImmutableMap cardinalMap; + private final ImmutableMap dayOfWeek; + private final ImmutableMap monthOfYear; + private final ImmutableMap numbers; + private final ImmutableMap doubleNumbers; + private final ImmutableMap writtenDecades; + private final ImmutableMap specialDecadeCases; + + private final IExtractor cardinalExtractor; + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IParser numberParser; + + private final IDateTimeExtractor durationExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor datePeriodExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor dateTimePeriodExtractor; + + private final IDateTimeParser timeZoneParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser durationParser; + private final IDateTimeParser datePeriodParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser dateTimePeriodParser; + private final IDateTimeParser dateTimeAltParser; + + public SpanishCommonDateTimeParserConfiguration(DateTimeOptions options) { + + super(options); + + utilityConfiguration = new SpanishDatetimeUtilityConfiguration(); + + unitMap = SpanishDateTime.UnitMap; + unitValueMap = SpanishDateTime.UnitValueMap; + seasonMap = SpanishDateTime.SeasonMap; + specialYearPrefixesMap = SpanishDateTime.SpecialYearPrefixesMap; + cardinalMap = SpanishDateTime.CardinalMap; + dayOfWeek = SpanishDateTime.DayOfWeek; + monthOfYear = SpanishDateTime.MonthOfYear; + numbers = SpanishDateTime.Numbers; + doubleNumbers = SpanishDateTime.DoubleNumbers; + writtenDecades = SpanishDateTime.WrittenDecades; + specialDecadeCases = SpanishDateTime.SpecialDecadeCases; + + cardinalExtractor = CardinalExtractor.getInstance(); + integerExtractor = IntegerExtractor.getInstance(); + ordinalExtractor = OrdinalExtractor.getInstance(); + + numberParser = new BaseNumberParser(new SpanishNumberParserConfiguration()); + + dateExtractor = new BaseDateExtractor(new SpanishDateExtractorConfiguration(this)); + timeExtractor = new BaseTimeExtractor(new SpanishTimeExtractorConfiguration(options)); + dateTimeExtractor = new BaseDateTimeExtractor(new SpanishDateTimeExtractorConfiguration(options)); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration()); + datePeriodExtractor = new BaseDatePeriodExtractor(new SpanishDatePeriodExtractorConfiguration(this)); + timePeriodExtractor = new BaseTimePeriodExtractor(new SpanishTimePeriodExtractorConfiguration(options)); + dateTimePeriodExtractor = new BaseDateTimePeriodExtractor(new SpanishDateTimePeriodExtractorConfiguration(options)); + + timeZoneParser = new BaseTimeZoneParser(); + dateParser = new BaseDateParser(new SpanishDateParserConfiguration(this)); + timeParser = new TimeParser(new SpanishTimeParserConfiguration(this)); + dateTimeParser = new BaseDateTimeParser(new SpanishDateTimeParserConfiguration(this)); + durationParser = new BaseDurationParser(new SpanishDurationParserConfiguration(this)); + datePeriodParser = new BaseDatePeriodParser(new SpanishDatePeriodParserConfiguration(this)); + timePeriodParser = new BaseTimePeriodParser(new SpanishTimePeriodParserConfiguration(this)); + dateTimePeriodParser = new BaseDateTimePeriodParser(new SpanishDateTimePeriodParserConfiguration(this)); + dateTimeAltParser = new BaseDateTimeAltParser(new SpanishDateTimeAltParserConfiguration(this)); + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } + + @Override + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + @Override + public IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + @Override + public IDateTimeParser getDateTimeAltParser() { + return dateTimeAltParser; + } + + @Override public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } + + @Override + public ImmutableMap getSeasonMap() { + return seasonMap; + } + + @Override + public ImmutableMap getSpecialYearPrefixesMap() { + return specialYearPrefixesMap; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public ImmutableMap getDayOfWeek() { + return dayOfWeek; + } + + @Override + public ImmutableMap getDoubleNumbers() { + return doubleNumbers; + } + + @Override + public ImmutableMap getWrittenDecades() { + return writtenDecades; + } + + @Override + public ImmutableMap getSpecialDecadeCases() { + return specialDecadeCases; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateParserConfiguration.java new file mode 100644 index 000000000..858415171 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateParserConfiguration.java @@ -0,0 +1,336 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.StringExtension; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpanishDateParserConfiguration extends BaseOptionsConfiguration implements IDateParserConfiguration { + + private final String dateTokenPrefix; + private final IExtractor integerExtractor; + private final IExtractor ordinalExtractor; + private final IExtractor cardinalExtractor; + private final IParser numberParser; + private final IDateTimeExtractor durationExtractor; + private final IDateExtractor dateExtractor; + private final IDateTimeParser durationParser; + private final ImmutableMap unitMap; + private final Iterable dateRegexes; + private final Pattern onRegex; + private final Pattern specialDayRegex; + private final Pattern specialDayWithNumRegex; + private final Pattern nextRegex; + private final Pattern thisRegex; + private final Pattern lastRegex; + private final Pattern unitRegex; + private final Pattern weekDayRegex; + private final Pattern monthRegex; + private final Pattern weekDayOfMonthRegex; + private final Pattern forTheRegex; + private final Pattern weekDayAndDayOfMonthRegex; + private final Pattern relativeMonthRegex; + private final Pattern strictRelativeRegex; + private final Pattern yearSuffix; + private final Pattern relativeWeekDayRegex; + private final Pattern relativeDayRegex; + private final Pattern nextPrefixRegex; + private final Pattern previousPrefixRegex; + + private final ImmutableMap dayOfMonth; + private final ImmutableMap dayOfWeek; + private final ImmutableMap monthOfYear; + private final ImmutableMap cardinalMap; + private final List sameDayTerms; + private final List plusOneDayTerms; + private final List plusTwoDayTerms; + private final List minusOneDayTerms; + private final List minusTwoDayTerms; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + public SpanishDateParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + dateTokenPrefix = SpanishDateTime.DateTokenPrefix; + integerExtractor = config.getIntegerExtractor(); + ordinalExtractor = config.getOrdinalExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = config.getDurationExtractor(); + dateExtractor = config.getDateExtractor(); + durationParser = config.getDurationParser(); + dateRegexes = Collections.unmodifiableList(SpanishDateExtractorConfiguration.DateRegexList); + onRegex = SpanishDateExtractorConfiguration.OnRegex; + specialDayRegex = SpanishDateExtractorConfiguration.SpecialDayRegex; + specialDayWithNumRegex = SpanishDateExtractorConfiguration.SpecialDayWithNumRegex; + nextRegex = SpanishDateExtractorConfiguration.NextDateRegex; + thisRegex = SpanishDateExtractorConfiguration.ThisRegex; + lastRegex = SpanishDateExtractorConfiguration.LastDateRegex; + unitRegex = SpanishDateExtractorConfiguration.DateUnitRegex; + weekDayRegex = SpanishDateExtractorConfiguration.WeekDayRegex; + monthRegex = SpanishDateExtractorConfiguration.MonthRegex; + weekDayOfMonthRegex = SpanishDateExtractorConfiguration.WeekDayOfMonthRegex; + forTheRegex = SpanishDateExtractorConfiguration.ForTheRegex; + weekDayAndDayOfMonthRegex = SpanishDateExtractorConfiguration.WeekDayAndDayOfMonthRegex; + relativeMonthRegex = SpanishDateExtractorConfiguration.RelativeMonthRegex; + strictRelativeRegex = SpanishDateExtractorConfiguration.StrictRelativeRegex; + yearSuffix = SpanishDateExtractorConfiguration.YearSuffix; + relativeWeekDayRegex = SpanishDateExtractorConfiguration.RelativeWeekDayRegex; + relativeDayRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeDayRegex); + nextPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NextPrefixRegex); + previousPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PreviousPrefixRegex); + dayOfMonth = config.getDayOfMonth(); + dayOfWeek = config.getDayOfWeek(); + monthOfYear = config.getMonthOfYear(); + cardinalMap = config.getCardinalMap(); + unitMap = config.getUnitMap(); + utilityConfiguration = config.getUtilityConfiguration(); + sameDayTerms = Collections.unmodifiableList(SpanishDateTime.SameDayTerms); + plusOneDayTerms = Collections.unmodifiableList(SpanishDateTime.PlusOneDayTerms); + plusTwoDayTerms = Collections.unmodifiableList(SpanishDateTime.PlusTwoDayTerms); + minusOneDayTerms = Collections.unmodifiableList(SpanishDateTime.MinusOneDayTerms); + minusTwoDayTerms = Collections.unmodifiableList(SpanishDateTime.MinusTwoDayTerms); + } + + @Override + public String getDateTokenPrefix() { + return dateTokenPrefix; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public Iterable getDateRegexes() { + return dateRegexes; + } + + @Override + public Pattern getOnRegex() { + return onRegex; + } + + @Override + public Pattern getSpecialDayRegex() { + return specialDayRegex; + } + + @Override + public Pattern getSpecialDayWithNumRegex() { + return specialDayWithNumRegex; + } + + @Override + public Pattern getNextRegex() { + return nextRegex; + } + + @Override + public Pattern getThisRegex() { + return thisRegex; + } + + @Override + public Pattern getLastRegex() { + return lastRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getWeekDayRegex() { + return weekDayRegex; + } + + @Override + public Pattern getMonthRegex() { + return monthRegex; + } + + @Override + public Pattern getWeekDayOfMonthRegex() { + return weekDayOfMonthRegex; + } + + @Override + public Pattern getForTheRegex() { + return forTheRegex; + } + + @Override + public Pattern getWeekDayAndDayOfMonthRegex() { + return weekDayAndDayOfMonthRegex; + } + + @Override + public Pattern getRelativeMonthRegex() { + return relativeMonthRegex; + } + + @Override + public Pattern getStrictRelativeRegex() { + return strictRelativeRegex; + } + + @Override + public Pattern getYearSuffix() { + return yearSuffix; + } + + @Override + public Pattern getRelativeWeekDayRegex() { + return relativeWeekDayRegex; + } + + @Override + public Pattern getRelativeDayRegex() { + return relativeDayRegex; + } + + @Override + public Pattern getNextPrefixRegex() { + return nextPrefixRegex; + } + + @Override + public Pattern getPastPrefixRegex() { + return previousPrefixRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } + + @Override + public ImmutableMap getDayOfWeek() { + return dayOfWeek; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public List getSameDayTerms() { + return sameDayTerms; + } + + @Override + public List getPlusOneDayTerms() { + return plusOneDayTerms; + } + + @Override + public List getMinusOneDayTerms() { + return minusOneDayTerms; + } + + @Override + public List getPlusTwoDayTerms() { + return plusTwoDayTerms; + } + + @Override + public List getMinusTwoDayTerms() { + return minusTwoDayTerms; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public Integer getSwiftMonthOrYear(String text) { + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + int swift = 0; + + Matcher regexMatcher = nextPrefixRegex.matcher(trimmedText); + if (regexMatcher.find()) { + swift = 1; + } + + regexMatcher = previousPrefixRegex.matcher(trimmedText); + if (regexMatcher.find()) { + swift = -1; + } + + return swift; + } + + @Override + public Boolean isCardinalLast(String text) { + String trimmedText = text.trim().toLowerCase(); + return trimmedText.equals("last"); + } + + @Override + public String normalize(String text) { + return StringExtension.normalize(text, SpanishDateTime.SpecialCharactersEquivalent); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDatePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDatePeriodParserConfiguration.java new file mode 100644 index 000000000..6cd0032f1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDatePeriodParserConfiguration.java @@ -0,0 +1,621 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDatePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.EnglishDateTime; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDurationExtractorConfiguration; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public class SpanishDatePeriodParserConfiguration extends BaseOptionsConfiguration implements IDatePeriodParserConfiguration { + + public static final Pattern nextPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NextPrefixRegex); + public static final Pattern nextSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NextSuffixRegex); + public static final Pattern previousPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PreviousPrefixRegex); + public static final Pattern previousSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PreviousSuffixRegex); + public static final Pattern thisPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.ThisPrefixRegex); + public static final Pattern relativeSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeSuffixRegex); + public static final Pattern relativeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RelativeRegex); + public static final Pattern unspecificEndOfRangeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.UnspecificEndOfRangeRegex); + + public SpanishDatePeriodParserConfiguration(ICommonDateTimeParserConfiguration config) { + super(config.getOptions()); + + tokenBeforeDate = SpanishDateTime.TokenBeforeDate; + cardinalExtractor = config.getCardinalExtractor(); + ordinalExtractor = config.getOrdinalExtractor(); + integerExtractor = config.getIntegerExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = config.getDurationExtractor(); + dateExtractor = config.getDateExtractor(); + durationParser = config.getDurationParser(); + dateParser = config.getDateParser(); + monthFrontBetweenRegex = SpanishDatePeriodExtractorConfiguration.MonthFrontBetweenRegex; + betweenRegex = SpanishDatePeriodExtractorConfiguration.DayBetweenRegex; + monthFrontSimpleCasesRegex = SpanishDatePeriodExtractorConfiguration.MonthFrontSimpleCasesRegex; + simpleCasesRegex = SpanishDatePeriodExtractorConfiguration.SimpleCasesRegex; + oneWordPeriodRegex = SpanishDatePeriodExtractorConfiguration.OneWordPeriodRegex; + monthWithYear = SpanishDatePeriodExtractorConfiguration.MonthWithYearRegex; + monthNumWithYear = SpanishDatePeriodExtractorConfiguration.MonthNumWithYearRegex; + yearRegex = SpanishDatePeriodExtractorConfiguration.YearRegex; + pastRegex = SpanishDatePeriodExtractorConfiguration.PastRegex; + futureRegex = SpanishDatePeriodExtractorConfiguration.FutureRegex; + futureSuffixRegex = SpanishDatePeriodExtractorConfiguration.FutureSuffixRegex; + numberCombinedWithUnit = SpanishDurationExtractorConfiguration.NumberCombinedWithUnit; + weekOfMonthRegex = SpanishDatePeriodExtractorConfiguration.WeekOfMonthRegex; + weekOfYearRegex = SpanishDatePeriodExtractorConfiguration.WeekOfYearRegex; + quarterRegex = SpanishDatePeriodExtractorConfiguration.QuarterRegex; + quarterRegexYearFront = SpanishDatePeriodExtractorConfiguration.QuarterRegexYearFront; + allHalfYearRegex = SpanishDatePeriodExtractorConfiguration.AllHalfYearRegex; + seasonRegex = SpanishDatePeriodExtractorConfiguration.SeasonRegex; + whichWeekRegex = SpanishDatePeriodExtractorConfiguration.WhichWeekRegex; + weekOfRegex = SpanishDatePeriodExtractorConfiguration.WeekOfRegex; + monthOfRegex = SpanishDatePeriodExtractorConfiguration.MonthOfRegex; + restOfDateRegex = SpanishDatePeriodExtractorConfiguration.RestOfDateRegex; + laterEarlyPeriodRegex = SpanishDatePeriodExtractorConfiguration.LaterEarlyPeriodRegex; + weekWithWeekDayRangeRegex = SpanishDatePeriodExtractorConfiguration.WeekWithWeekDayRangeRegex; + yearPlusNumberRegex = SpanishDatePeriodExtractorConfiguration.YearPlusNumberRegex; + decadeWithCenturyRegex = SpanishDatePeriodExtractorConfiguration.DecadeWithCenturyRegex; + yearPeriodRegex = SpanishDatePeriodExtractorConfiguration.YearPeriodRegex; + complexDatePeriodRegex = SpanishDatePeriodExtractorConfiguration.ComplexDatePeriodRegex; + relativeDecadeRegex = SpanishDatePeriodExtractorConfiguration.RelativeDecadeRegex; + inConnectorRegex = config.getUtilityConfiguration().getInConnectorRegex(); + withinNextPrefixRegex = SpanishDatePeriodExtractorConfiguration.WithinNextPrefixRegex; + referenceDatePeriodRegex = SpanishDatePeriodExtractorConfiguration.ReferenceDatePeriodRegex; + agoRegex = SpanishDatePeriodExtractorConfiguration.AgoRegex; + laterRegex = SpanishDatePeriodExtractorConfiguration.LaterRegex; + lessThanRegex = SpanishDatePeriodExtractorConfiguration.LessThanRegex; + moreThanRegex = SpanishDatePeriodExtractorConfiguration.MoreThanRegex; + centurySuffixRegex = SpanishDatePeriodExtractorConfiguration.CenturySuffixRegex; + nowRegex = SpanishDatePeriodExtractorConfiguration.NowRegex; + + unitMap = config.getUnitMap(); + cardinalMap = config.getCardinalMap(); + dayOfMonth = config.getDayOfMonth(); + monthOfYear = config.getMonthOfYear(); + seasonMap = config.getSeasonMap(); + specialYearPrefixesMap = config.getSpecialYearPrefixesMap(); + numbers = config.getNumbers(); + writtenDecades = config.getWrittenDecades(); + specialDecadeCases = config.getSpecialDecadeCases(); + + afterNextSuffixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AfterNextSuffixRegex); + } + + // Regex + + private final String tokenBeforeDate; + + private final IDateExtractor dateExtractor; + + private final IExtractor cardinalExtractor; + + private final IExtractor ordinalExtractor; + + private final IDateTimeExtractor durationExtractor; + + private final IExtractor integerExtractor; + + private final IParser numberParser; + + private final IDateTimeParser dateParser; + + private final IDateTimeParser durationParser; + + private final Pattern monthFrontBetweenRegex; + + private final Pattern betweenRegex; + + private final Pattern monthFrontSimpleCasesRegex; + + private final Pattern simpleCasesRegex; + + private final Pattern oneWordPeriodRegex; + + private final Pattern monthWithYear; + + private final Pattern monthNumWithYear; + + private final Pattern yearRegex; + + private final Pattern pastRegex; + + private final Pattern futureRegex; + + private final Pattern futureSuffixRegex; + + private final Pattern numberCombinedWithUnit; + + private final Pattern weekOfMonthRegex; + + private final Pattern weekOfYearRegex; + + private final Pattern quarterRegex; + + private final Pattern quarterRegexYearFront; + + private final Pattern allHalfYearRegex; + + private final Pattern seasonRegex; + + private final Pattern whichWeekRegex; + + private final Pattern weekOfRegex; + + private final Pattern monthOfRegex; + + private final Pattern inConnectorRegex; + + private final Pattern withinNextPrefixRegex; + + private final Pattern restOfDateRegex; + + private final Pattern laterEarlyPeriodRegex; + + private final Pattern weekWithWeekDayRangeRegex; + + private final Pattern yearPlusNumberRegex; + + private final Pattern decadeWithCenturyRegex; + + private final Pattern yearPeriodRegex; + + private final Pattern complexDatePeriodRegex; + + private final Pattern relativeDecadeRegex; + + private final Pattern referenceDatePeriodRegex; + + private final Pattern agoRegex; + + private final Pattern laterRegex; + + private final Pattern lessThanRegex; + + private final Pattern moreThanRegex; + + private final Pattern centurySuffixRegex; + + private final Pattern afterNextSuffixRegex; + + private final Pattern nowRegex; + + // Dictionaries + private final ImmutableMap unitMap; + private final ImmutableMap cardinalMap; + private final ImmutableMap dayOfMonth; + private final ImmutableMap monthOfYear; + private final ImmutableMap seasonMap; + private final ImmutableMap specialYearPrefixesMap; + private final ImmutableMap writtenDecades; + private final ImmutableMap numbers; + private final ImmutableMap specialDecadeCases; + + @Override + public final String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public final IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public final IExtractor getOrdinalExtractor() { + return ordinalExtractor; + } + + @Override + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public final IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public final IParser getNumberParser() { + return numberParser; + } + + @Override + public final IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public final IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public final Pattern getMonthFrontBetweenRegex() { + return monthFrontBetweenRegex; + } + + @Override + public final Pattern getBetweenRegex() { + return betweenRegex; + } + + @Override + public final Pattern getMonthFrontSimpleCasesRegex() { + return monthFrontSimpleCasesRegex; + } + + @Override + public final Pattern getSimpleCasesRegex() { + return simpleCasesRegex; + } + + @Override + public final Pattern getOneWordPeriodRegex() { + return oneWordPeriodRegex; + } + + @Override + public final Pattern getMonthWithYear() { + return monthWithYear; + } + + @Override + public final Pattern getMonthNumWithYear() { + return monthNumWithYear; + } + + @Override + public final Pattern getYearRegex() { + return yearRegex; + } + + @Override + public final Pattern getPastRegex() { + return pastRegex; + } + + @Override + public final Pattern getFutureRegex() { + return futureRegex; + } + + @Override + public final Pattern getFutureSuffixRegex() { + return futureSuffixRegex; + } + + @Override + public final Pattern getNumberCombinedWithUnit() { + return numberCombinedWithUnit; + } + + @Override + public final Pattern getWeekOfMonthRegex() { + return weekOfMonthRegex; + } + + @Override + public final Pattern getWeekOfYearRegex() { + return weekOfYearRegex; + } + + @Override + public final Pattern getQuarterRegex() { + return quarterRegex; + } + + @Override + public final Pattern getQuarterRegexYearFront() { + return quarterRegexYearFront; + } + + @Override + public final Pattern getAllHalfYearRegex() { + return allHalfYearRegex; + } + + @Override + public final Pattern getSeasonRegex() { + return seasonRegex; + } + + @Override + public final Pattern getWhichWeekRegex() { + return whichWeekRegex; + } + + @Override + public final Pattern getWeekOfRegex() { + return weekOfRegex; + } + + @Override + public final Pattern getMonthOfRegex() { + return monthOfRegex; + } + + @Override + public final Pattern getInConnectorRegex() { + return inConnectorRegex; + } + + @Override + public final Pattern getWithinNextPrefixRegex() { + return withinNextPrefixRegex; + } + + @Override + public final Pattern getRestOfDateRegex() { + return restOfDateRegex; + } + + @Override + public final Pattern getLaterEarlyPeriodRegex() { + return laterEarlyPeriodRegex; + } + + @Override + public final Pattern getWeekWithWeekDayRangeRegex() { + return laterEarlyPeriodRegex; + } + + @Override + public final Pattern getYearPlusNumberRegex() { + return yearPlusNumberRegex; + } + + @Override + public final Pattern getDecadeWithCenturyRegex() { + return decadeWithCenturyRegex; + } + + @Override + public final Pattern getYearPeriodRegex() { + return yearPeriodRegex; + } + + @Override + public final Pattern getComplexDatePeriodRegex() { + return complexDatePeriodRegex; + } + + @Override + public final Pattern getRelativeDecadeRegex() { + return complexDatePeriodRegex; + } + + @Override + public final Pattern getReferenceDatePeriodRegex() { + return referenceDatePeriodRegex; + } + + @Override + public final Pattern getAgoRegex() { + return agoRegex; + } + + @Override + public final Pattern getLaterRegex() { + return laterRegex; + } + + @Override + public final Pattern getLessThanRegex() { + return lessThanRegex; + } + + @Override + public final Pattern getMoreThanRegex() { + return moreThanRegex; + } + + @Override + public final Pattern getCenturySuffixRegex() { + return centurySuffixRegex; + } + + @Override + public final Pattern getNextPrefixRegex() { + return nextPrefixRegex; + } + + @Override + public final Pattern getPastPrefixRegex() { + return previousPrefixRegex; + } + + @Override + public final Pattern getThisPrefixRegex() { + return thisPrefixRegex; + } + + @Override + public final Pattern getRelativeRegex() { + return relativeRegex; + } + + @Override + public final Pattern getUnspecificEndOfRangeRegex() { + return unspecificEndOfRangeRegex; + } + + @Override + public Pattern getNowRegex() { + return nowRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getCardinalMap() { + return cardinalMap; + } + + @Override + public ImmutableMap getDayOfMonth() { + return dayOfMonth; + } + + @Override + public ImmutableMap getMonthOfYear() { + return monthOfYear; + } + + @Override + public ImmutableMap getSeasonMap() { + return seasonMap; + } + + @Override + public ImmutableMap getSpecialYearPrefixesMap() { + return specialYearPrefixesMap; + } + + @Override + public ImmutableMap getWrittenDecades() { + return writtenDecades; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public ImmutableMap getSpecialDecadeCases() { + return specialDecadeCases; + } + + @Override + public int getSwiftDayOrMonth(String text) { + + String trimmedText = text.trim().toLowerCase(); + int swift = 0; + + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + Optional matchNextPrefix = Arrays.stream(RegExpUtility.getMatches(nextPrefixRegex, trimmedText)).findFirst(); + Optional matchNextSuffix = Arrays.stream(RegExpUtility.getMatches(nextSuffixRegex, trimmedText)).findFirst(); + Optional matchPastPrefix = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)).findFirst(); + Optional matchPastSuffix = Arrays.stream(RegExpUtility.getMatches(previousSuffixRegex, trimmedText)).findFirst(); + + if (matchAfterNext.isPresent()) { + swift = 2; + } else if (matchNextPrefix.isPresent() || matchNextSuffix.isPresent()) { + swift = 1; + } else if (matchPastPrefix.isPresent() || matchPastSuffix.isPresent()) { + swift = -1; + } + + return swift; + } + + @Override + public int getSwiftYear(String text) { + + String trimmedText = text.trim().toLowerCase(); + int swift = -10; + + Optional matchAfterNext = Arrays.stream(RegExpUtility.getMatches(afterNextSuffixRegex, trimmedText)).findFirst(); + Optional matchNextPrefix = Arrays.stream(RegExpUtility.getMatches(nextPrefixRegex, trimmedText)).findFirst(); + Optional matchNextSuffix = Arrays.stream(RegExpUtility.getMatches(nextSuffixRegex, trimmedText)).findFirst(); + Optional matchPastPrefix = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)).findFirst(); + Optional matchPastSuffix = Arrays.stream(RegExpUtility.getMatches(previousSuffixRegex, trimmedText)).findFirst(); + Optional matchThisPresent = Arrays.stream(RegExpUtility.getMatches(thisPrefixRegex, trimmedText)).findFirst(); + + if (matchAfterNext.isPresent()) { + swift = 2; + } else if (matchNextPrefix.isPresent() || matchNextSuffix.isPresent()) { + swift = 1; + } else if (matchPastPrefix.isPresent() || matchPastSuffix.isPresent()) { + swift = -1; + } else if (matchThisPresent.isPresent()) { + swift = 0; + } + + return swift; + } + + @Override + public boolean isFuture(String text) { + String trimmedText = text.trim().toLowerCase(); + + Optional matchThis = Arrays.stream(RegExpUtility.getMatches(thisPrefixRegex, trimmedText)).findFirst(); + Optional matchNext = Arrays.stream(RegExpUtility.getMatches(nextPrefixRegex, trimmedText)).findFirst(); + return matchThis.isPresent() || matchNext.isPresent(); + } + + @Override + public boolean isLastCardinal(String text) { + String trimmedText = text.trim().toLowerCase(); + + Optional matchLast = Arrays.stream(RegExpUtility.getMatches(previousPrefixRegex, trimmedText)).findFirst(); + return matchLast.isPresent(); + } + + @Override + public boolean isMonthOnly(String text) { + String trimmedText = text.trim().toLowerCase(); + Optional matchRelative = Arrays.stream(RegExpUtility.getMatches(relativeSuffixRegex, trimmedText)).findFirst(); + return SpanishDateTime.MonthTerms.stream().anyMatch(o -> trimmedText.endsWith(o)) || + SpanishDateTime.MonthTerms.stream().anyMatch(o -> trimmedText.contains(o)) && matchRelative.isPresent(); + } + + @Override + public boolean isMonthToDate(String text) { + String trimmedText = text.trim().toLowerCase(); + return SpanishDateTime.MonthToDateTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isWeekend(String text) { + String trimmedText = text.trim().toLowerCase(); + Optional matchRelative = Arrays.stream(RegExpUtility.getMatches(relativeSuffixRegex, trimmedText)).findFirst(); + return SpanishDateTime.WeekendTerms.stream().anyMatch(o -> trimmedText.endsWith(o)) || + SpanishDateTime.WeekendTerms.stream().anyMatch(o -> trimmedText.contains(o)) && matchRelative.isPresent(); + } + + @Override + public boolean isWeekOnly(String text) { + String trimmedText = text.trim().toLowerCase(); + Optional matchRelative = Arrays.stream(RegExpUtility.getMatches(relativeSuffixRegex, trimmedText)).findFirst(); + return (SpanishDateTime.WeekTerms.stream().anyMatch(o -> trimmedText.endsWith(o)) || + SpanishDateTime.WeekTerms.stream().anyMatch(o -> trimmedText.contains(o)) && matchRelative.isPresent()) && + !SpanishDateTime.WeekendTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isYearOnly(String text) { + String trimmedText = text.trim().toLowerCase(); + return SpanishDateTime.YearTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } + + @Override + public boolean isYearToDate(String text) { + String trimmedText = text.trim().toLowerCase(); + + return SpanishDateTime.YearToDateTerms.stream().anyMatch(o -> trimmedText.endsWith(o)); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimeAltParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimeAltParserConfiguration.java new file mode 100644 index 000000000..38020f75f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimeAltParserConfiguration.java @@ -0,0 +1,48 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeAltParserConfiguration; + +public class SpanishDateTimeAltParserConfiguration implements IDateTimeAltParserConfiguration { + + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimePeriodParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser datePeriodParser; + + public SpanishDateTimeAltParserConfiguration(ICommonDateTimeParserConfiguration config) { + dateTimeParser = config.getDateTimeParser(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + dateTimePeriodParser = config.getDateTimePeriodParser(); + timePeriodParser = config.getTimePeriodParser(); + datePeriodParser = config.getDatePeriodParser(); + } + + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + public IDateTimeParser getDateParser() { + return dateParser; + } + + public IDateTimeParser getTimeParser() { + return timeParser; + } + + public IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + public IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimeParserConfiguration.java new file mode 100644 index 000000000..8ef21ca4d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimeParserConfiguration.java @@ -0,0 +1,245 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.extractors.config.ResultTimex; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpanishDateTimeParserConfiguration extends BaseOptionsConfiguration implements IDateTimeParserConfiguration { + + public final String tokenBeforeDate; + public final String tokenBeforeTime; + + public final IDateTimeExtractor dateExtractor; + public final IDateTimeExtractor timeExtractor; + public final IDateTimeParser dateParser; + public final IDateTimeParser timeParser; + public final IExtractor cardinalExtractor; + public final IExtractor integerExtractor; + public final IParser numberParser; + public final IDateTimeExtractor durationExtractor; + public final IDateTimeParser durationParser; + + public final ImmutableMap unitMap; + public final ImmutableMap numbers; + + public final Pattern nowRegex; + public final Pattern amTimeRegex; + public final Pattern pmTimeRegex; + public final Pattern lastNightTimeRegex; + public final Pattern simpleTimeOfTodayAfterRegex; + public final Pattern simpleTimeOfTodayBeforeRegex; + public final Pattern specificTimeOfDayRegex; + public final Pattern specificEndOfRegex; + public final Pattern unspecificEndOfRegex; + public final Pattern unitRegex; + public final Pattern dateNumberConnectorRegex; + + public final IDateTimeUtilityConfiguration utilityConfiguration; + + public SpanishDateTimeParserConfiguration(ICommonDateTimeParserConfiguration config) { + super(config.getOptions()); + + unitMap = config.getUnitMap(); + numbers = config.getNumbers(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + numberParser = config.getNumberParser(); + dateExtractor = config.getDateExtractor(); + timeExtractor = config.getTimeExtractor(); + durationParser = config.getDurationParser(); + integerExtractor = config.getIntegerExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + durationExtractor = config.getDurationExtractor(); + utilityConfiguration = config.getUtilityConfiguration(); + + tokenBeforeDate = SpanishDateTime.TokenBeforeDate; + tokenBeforeTime = SpanishDateTime.TokenBeforeTime; + + nowRegex = SpanishDateTimeExtractorConfiguration.NowRegex; + unitRegex = SpanishDateTimeExtractorConfiguration.UnitRegex; + specificEndOfRegex = SpanishDateTimeExtractorConfiguration.SpecificEndOfRegex; + unspecificEndOfRegex = SpanishDateTimeExtractorConfiguration.UnspecificEndOfRegex; + specificTimeOfDayRegex = SpanishDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + dateNumberConnectorRegex = SpanishDateTimeExtractorConfiguration.DateNumberConnectorRegex; + simpleTimeOfTodayAfterRegex = SpanishDateTimeExtractorConfiguration.SimpleTimeOfTodayAfterRegex; + simpleTimeOfTodayBeforeRegex = SpanishDateTimeExtractorConfiguration.SimpleTimeOfTodayBeforeRegex; + + pmTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PmRegex); + amTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AmTimeRegex); + lastNightTimeRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LastNightTimeRegex); + } + + @Override + public int getHour(String text, int hour) { + + String trimmedText = text.trim().toLowerCase(); + int result = hour; + + //TODO: Replace with a regex + if ((trimmedText.endsWith("mañana") || trimmedText.endsWith("madrugada")) && hour >= Constants.HalfDayHourCount) { + result -= Constants.HalfDayHourCount; + } else if (!(trimmedText.endsWith("mañana") || trimmedText.endsWith("madrugada")) && hour < Constants.HalfDayHourCount) { + result += Constants.HalfDayHourCount; + } + + return result; + } + + @Override + public ResultTimex getMatchedNowTimex(String text) { + + String trimmedText = text.trim().toLowerCase(); + + if (trimmedText.endsWith("ahora") || trimmedText.endsWith("mismo") || trimmedText.endsWith("momento")) { + return new ResultTimex(true, "PRESENT_REF"); + } else if (trimmedText.endsWith("posible") || trimmedText.endsWith("pueda") || trimmedText.endsWith("puedas") || + trimmedText.endsWith("podamos") || trimmedText.endsWith("puedan")) { + return new ResultTimex(true, "FUTURE_REF"); + } else if (trimmedText.endsWith("mente")) { + return new ResultTimex(true, "PAST_REF"); + } + + return new ResultTimex(false, null); + } + + @Override + public int getSwiftDay(String text) { + + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + Matcher regexMatcher = SpanishDatePeriodParserConfiguration.previousPrefixRegex.matcher(trimmedText); + + int swift = 0; + + if (regexMatcher.find()) { + swift = -1; + } else { + regexMatcher = this.lastNightTimeRegex.matcher(trimmedText); + if (regexMatcher.find()) { + swift = -1; + } else { + regexMatcher = SpanishDatePeriodParserConfiguration.nextPrefixRegex.matcher(trimmedText); + if (regexMatcher.find()) { + swift = 1; + } + } + } + + return swift; + } + + @Override + public boolean containsAmbiguousToken(String text, String matchedText) { + return text.contains("esta mañana") && matchedText.contains("mañana"); + } + + @Override public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override public String getTokenBeforeTime() { + return tokenBeforeTime; + } + + @Override public IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + @Override public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override public IParser getNumberParser() { + return numberParser; + } + + @Override public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override public ImmutableMap getNumbers() { + return numbers; + } + + @Override public Pattern getNowRegex() { + return nowRegex; + } + + public Pattern getAMTimeRegex() { + return amTimeRegex; + } + + public Pattern getPMTimeRegex() { + return pmTimeRegex; + } + + @Override public Pattern getSimpleTimeOfTodayAfterRegex() { + return simpleTimeOfTodayAfterRegex; + } + + @Override public Pattern getSimpleTimeOfTodayBeforeRegex() { + return simpleTimeOfTodayBeforeRegex; + } + + @Override public Pattern getSpecificTimeOfDayRegex() { + return specificTimeOfDayRegex; + } + + @Override public Pattern getSpecificEndOfRegex() { + return specificEndOfRegex; + } + + @Override public Pattern getUnspecificEndOfRegex() { + return unspecificEndOfRegex; + } + + @Override public Pattern getUnitRegex() { + return unitRegex; + } + + @Override public Pattern getDateNumberConnectorRegex() { + return dateNumberConnectorRegex; + } + + @Override public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimePeriodParserConfiguration.java new file mode 100644 index 000000000..76ed8cec5 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDateTimePeriodParserConfiguration.java @@ -0,0 +1,347 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDateTimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDateTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpanishDateTimePeriodParserConfiguration extends BaseOptionsConfiguration implements IDateTimePeriodParserConfiguration { + + private final String tokenBeforeDate; + + private final IDateTimeExtractor dateExtractor; + private final IDateTimeExtractor timeExtractor; + private final IDateTimeExtractor dateTimeExtractor; + private final IDateTimeExtractor timePeriodExtractor; + private final IDateTimeExtractor durationExtractor; + private final IExtractor cardinalExtractor; + + private final IParser numberParser; + private final IDateTimeParser dateParser; + private final IDateTimeParser timeParser; + private final IDateTimeParser dateTimeParser; + private final IDateTimeParser timePeriodParser; + private final IDateTimeParser durationParser; + private final IDateTimeParser timeZoneParser; + + private final Pattern pureNumberFromToRegex; + private final Pattern pureNumberBetweenAndRegex; + private final Pattern specificTimeOfDayRegex; + private final Pattern timeOfDayRegex; + private final Pattern pastRegex; + private final Pattern futureRegex; + private final Pattern futureSuffixRegex; + private final Pattern numberCombinedWithUnitRegex; + private final Pattern unitRegex; + private final Pattern periodTimeOfDayWithDateRegex; + private final Pattern relativeTimeUnitRegex; + private final Pattern restOfDateTimeRegex; + private final Pattern amDescRegex; + private final Pattern pmDescRegex; + private final Pattern withinNextPrefixRegex; + private final Pattern prefixDayRegex; + private final Pattern beforeRegex; + private final Pattern afterRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap numbers; + + /*public static final Pattern MorningStartEndRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.MorningStartEndRegex); + public static final Pattern AfternoonStartEndRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AfternoonStartEndRegex); + public static final Pattern EveningStartEndRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.EveningStartEndRegex); + public static final Pattern NightStartEndRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.NightStartEndRegex);*/ + + public SpanishDateTimePeriodParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + tokenBeforeDate = SpanishDateTime.TokenBeforeDate; + + dateExtractor = config.getDateExtractor(); + timeExtractor = config.getTimeExtractor(); + dateTimeExtractor = config.getDateTimeExtractor(); + timePeriodExtractor = config.getTimePeriodExtractor(); + cardinalExtractor = config.getCardinalExtractor(); + durationExtractor = config.getDurationExtractor(); + numberParser = config.getNumberParser(); + dateParser = config.getDateParser(); + timeParser = config.getTimeParser(); + timePeriodParser = config.getTimePeriodParser(); + durationParser = config.getDurationParser(); + dateTimeParser = config.getDateTimeParser(); + timeZoneParser = config.getTimeZoneParser(); + + pureNumberFromToRegex = SpanishTimePeriodExtractorConfiguration.PureNumFromTo; + pureNumberBetweenAndRegex = SpanishTimePeriodExtractorConfiguration.PureNumBetweenAnd; + specificTimeOfDayRegex = SpanishDateTimeExtractorConfiguration.SpecificTimeOfDayRegex; + timeOfDayRegex = SpanishDateTimeExtractorConfiguration.TimeOfDayRegex; + pastRegex = SpanishDatePeriodExtractorConfiguration.PastRegex; + futureRegex = SpanishDatePeriodExtractorConfiguration.FutureRegex; + futureSuffixRegex = SpanishDatePeriodExtractorConfiguration.FutureSuffixRegex; + numberCombinedWithUnitRegex = SpanishDateTimePeriodExtractorConfiguration.NumberCombinedWithUnit; + unitRegex = SpanishTimePeriodExtractorConfiguration.UnitRegex; + periodTimeOfDayWithDateRegex = SpanishDateTimePeriodExtractorConfiguration.PeriodTimeOfDayWithDateRegex; + relativeTimeUnitRegex = SpanishDateTimePeriodExtractorConfiguration.RelativeTimeUnitRegex; + restOfDateTimeRegex = SpanishDateTimePeriodExtractorConfiguration.RestOfDateTimeRegex; + amDescRegex = SpanishDateTimePeriodExtractorConfiguration.AmDescRegex; + pmDescRegex = SpanishDateTimePeriodExtractorConfiguration.PmDescRegex; + withinNextPrefixRegex = SpanishDateTimePeriodExtractorConfiguration.WithinNextPrefixRegex; + prefixDayRegex = SpanishDateTimePeriodExtractorConfiguration.PrefixDayRegex; + beforeRegex = SpanishDateTimePeriodExtractorConfiguration.BeforeRegex; + afterRegex = SpanishDateTimePeriodExtractorConfiguration.AfterRegex; + + unitMap = config.getUnitMap(); + numbers = config.getNumbers(); + } + + @Override + public String getTokenBeforeDate() { + return tokenBeforeDate; + } + + @Override + public IDateTimeExtractor getDateExtractor() { + return dateExtractor; + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + @Override + public IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + @Override + public IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public IDateTimeParser getDateParser() { + return dateParser; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + @Override + public IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + @Override + public IDateTimeParser getDurationParser() { + return durationParser; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public Pattern getPureNumberFromToRegex() { + return pureNumberFromToRegex; + } + + @Override + public Pattern getPureNumberBetweenAndRegex() { + return pureNumberBetweenAndRegex; + } + + @Override + public Pattern getSpecificTimeOfDayRegex() { + return specificTimeOfDayRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return timeOfDayRegex; + } + + @Override + public Pattern getPastRegex() { + return pastRegex; + } + + @Override + public Pattern getFutureRegex() { + return futureRegex; + } + + @Override + public Pattern getFutureSuffixRegex() { + return futureSuffixRegex; + } + + @Override + public Pattern getNumberCombinedWithUnitRegex() { + return numberCombinedWithUnitRegex; + } + + @Override + public Pattern getUnitRegex() { + return unitRegex; + } + + @Override + public Pattern getPeriodTimeOfDayWithDateRegex() { + return periodTimeOfDayWithDateRegex; + } + + @Override + public Pattern getRelativeTimeUnitRegex() { + return relativeTimeUnitRegex; + } + + @Override + public Pattern getRestOfDateTimeRegex() { + return restOfDateTimeRegex; + } + + @Override + public Pattern getAmDescRegex() { + return amDescRegex; + } + + @Override + public Pattern getPmDescRegex() { + return pmDescRegex; + } + + @Override + public Pattern getWithinNextPrefixRegex() { + return withinNextPrefixRegex; + } + + @Override + public Pattern getPrefixDayRegex() { + return prefixDayRegex; + } + + @Override + public Pattern getBeforeRegex() { + return beforeRegex; + } + + @Override + public Pattern getAfterRegex() { + return afterRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public MatchedTimeRangeResult getMatchedTimeRange(String text, String timeStr, int beginHour, int endHour, int endMin) { + + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + beginHour = 0; + endHour = 0; + endMin = 0; + boolean result = false; + + // TODO: modify it according to the coresponding function in English part + if (trimmedText.endsWith("madrugada")) { + timeStr = "TDA"; + beginHour = 4; + endHour = 8; + result = true; + } else if (trimmedText.endsWith("mañana")) { + timeStr = "TMO"; + beginHour = 8; + endHour = Constants.HalfDayHourCount; + result = true; + } else if (trimmedText.contains("pasado mediodia") || trimmedText.contains("pasado el mediodia")) { + timeStr = "TAF"; + beginHour = Constants.HalfDayHourCount; + endHour = 16; + result = true; + } else if (trimmedText.endsWith("tarde")) { + timeStr = "TEV"; + beginHour = 16; + endHour = 20; + result = true; + } else if (trimmedText.endsWith("noche")) { + timeStr = "TNI"; + beginHour = 20; + endHour = 23; + endMin = 59; + result = true; + } else { + timeStr = null; + } + + return new MatchedTimeRangeResult(result, timeStr, beginHour, endHour, endMin); + } + + @Override + public int getSwiftPrefix(String text) { + + String trimmedText = text.trim().toLowerCase(); + + Pattern regex = Pattern.compile(SpanishDateTime.PreviousPrefixRegex); + Matcher regexMatcher = regex.matcher(trimmedText); + + int swift = 0; + if (regexMatcher.find() || trimmedText.startsWith("anoche")) { + swift = -1; + } else { + regex = Pattern.compile(SpanishDateTime.NextPrefixRegex); + regexMatcher = regex.matcher(text); + if (regexMatcher.find()) { + swift = 1; + } + } + + return swift; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDurationParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDurationParserConfiguration.java new file mode 100644 index 000000000..0c7cd1487 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishDurationParserConfiguration.java @@ -0,0 +1,145 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.IParser; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.BaseDurationExtractor; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.IDurationParserConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDurationExtractorConfiguration; + +import java.util.regex.Pattern; + +public class SpanishDurationParserConfiguration extends BaseOptionsConfiguration implements IDurationParserConfiguration { + + private final IExtractor cardinalExtractor; + private final IExtractor durationExtractor; + private final IParser numberParser; + + private final Pattern numberCombinedWithUnit; + private final Pattern anUnitRegex; + private final Pattern duringRegex; + private final Pattern allDateUnitRegex; + private final Pattern halfDateUnitRegex; + private final Pattern suffixAndRegex; + private final Pattern followedUnit; + private final Pattern conjunctionRegex; + private final Pattern inexactNumberRegex; + private final Pattern inexactNumberUnitRegex; + private final Pattern durationUnitRegex; + + private final ImmutableMap unitMap; + private final ImmutableMap unitValueMap; + private final ImmutableMap doubleNumbers; + + public SpanishDurationParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + cardinalExtractor = config.getCardinalExtractor(); + numberParser = config.getNumberParser(); + durationExtractor = new BaseDurationExtractor(new SpanishDurationExtractorConfiguration(), false); + numberCombinedWithUnit = SpanishDurationExtractorConfiguration.NumberCombinedWithUnit; + + anUnitRegex = SpanishDurationExtractorConfiguration.AnUnitRegex; + duringRegex = SpanishDurationExtractorConfiguration.DuringRegex; + allDateUnitRegex = SpanishDurationExtractorConfiguration.AllRegex; + halfDateUnitRegex = SpanishDurationExtractorConfiguration.HalfRegex; + suffixAndRegex = SpanishDurationExtractorConfiguration.SuffixAndRegex; + followedUnit = SpanishDurationExtractorConfiguration.FollowedUnit; + conjunctionRegex = SpanishDurationExtractorConfiguration.ConjunctionRegex; + inexactNumberRegex = SpanishDurationExtractorConfiguration.InexactNumberRegex; + inexactNumberUnitRegex = SpanishDurationExtractorConfiguration.InexactNumberUnitRegex; + durationUnitRegex = SpanishDurationExtractorConfiguration.DurationUnitRegex; + + unitMap = config.getUnitMap(); + unitValueMap = config.getUnitValueMap(); + doubleNumbers = config.getDoubleNumbers(); + } + + @Override + public IExtractor getCardinalExtractor() { + return cardinalExtractor; + } + + @Override + public IExtractor getDurationExtractor() { + return durationExtractor; + } + + @Override + public IParser getNumberParser() { + return numberParser; + } + + @Override + public Pattern getNumberCombinedWithUnit() { + return numberCombinedWithUnit; + } + + @Override + public Pattern getAnUnitRegex() { + return anUnitRegex; + } + + @Override + public Pattern getDuringRegex() { + return duringRegex; + } + + @Override + public Pattern getAllDateUnitRegex() { + return allDateUnitRegex; + } + + @Override + public Pattern getHalfDateUnitRegex() { + return halfDateUnitRegex; + } + + @Override + public Pattern getSuffixAndRegex() { + return suffixAndRegex; + } + + @Override + public Pattern getFollowedUnit() { + return followedUnit; + } + + @Override + public Pattern getConjunctionRegex() { + return conjunctionRegex; + } + + @Override + public Pattern getInexactNumberRegex() { + return inexactNumberRegex; + } + + @Override + public Pattern getInexactNumberUnitRegex() { + return inexactNumberUnitRegex; + } + + @Override + public Pattern getDurationUnitRegex() { + return durationUnitRegex; + } + + @Override + public ImmutableMap getUnitMap() { + return unitMap; + } + + @Override + public ImmutableMap getUnitValueMap() { + return unitValueMap; + } + + @Override + public ImmutableMap getDoubleNumbers() { + return doubleNumbers; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishHolidayParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishHolidayParserConfiguration.java new file mode 100644 index 000000000..0eab930ff --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishHolidayParserConfiguration.java @@ -0,0 +1,137 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.parsers.BaseHolidayParserConfiguration; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishHolidayExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.DateUtil; +import com.microsoft.recognizers.text.datetime.utilities.HolidayFunctions; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.IntFunction; + +public class SpanishHolidayParserConfiguration extends BaseHolidayParserConfiguration { + + public SpanishHolidayParserConfiguration() { + + super(); + + this.setHolidayRegexList(SpanishHolidayExtractorConfiguration.HolidayRegexList); + + HashMap> holidayNamesMap = new HashMap<>(); + for (Map.Entry entry : SpanishDateTime.HolidayNames.entrySet()) { + if (entry.getValue() instanceof String[]) { + holidayNamesMap.put(entry.getKey(), Arrays.asList(entry.getValue())); + } + } + this.setHolidayNames(ImmutableMap.copyOf(holidayNamesMap)); + + HashMap variableHolidaysTimexMap = new HashMap<>(); + for (Map.Entry entry : SpanishDateTime.VariableHolidaysTimexDictionary.entrySet()) { + if (entry.getValue() instanceof String) { + variableHolidaysTimexMap.put(entry.getKey(), entry.getValue()); + } + } + this.setVariableHolidaysTimexDictionary(ImmutableMap.copyOf(variableHolidaysTimexMap)); + } + + @Override + public int getSwiftYear(String text) { + + String trimmedText = StringUtility + .trimStart(StringUtility.trimEnd(text)).toLowerCase(Locale.ROOT); + int swift = -10; + Optional matchNextPrefixRegex = Arrays.stream(RegExpUtility.getMatches( + SpanishDatePeriodParserConfiguration.nextPrefixRegex, text)).findFirst(); + Optional matchPastPrefixRegex = Arrays.stream(RegExpUtility.getMatches( + SpanishDatePeriodParserConfiguration.previousPrefixRegex, text)).findFirst(); + Optional matchThisPrefixRegex = Arrays.stream(RegExpUtility.getMatches( + SpanishDatePeriodParserConfiguration.thisPrefixRegex, text)).findFirst(); + if (matchNextPrefixRegex.isPresent() && matchNextPrefixRegex.get().length == text.trim().length()) { + swift = 1; + } else if (matchPastPrefixRegex.isPresent() && matchPastPrefixRegex.get().length == text.trim().length()) { + swift = -1; + } else if (matchThisPrefixRegex.isPresent() && matchThisPrefixRegex.get().length == text.trim().length()) { + swift = 0; + } + + return swift; + } + + public String sanitizeHolidayToken(String holiday) { + return holiday.replace(" ", "") + .replace("á", "a") + .replace("é", "e") + .replace("í", "i") + .replace("ó", "o") + .replace("ú", "u"); + } + + @Override + protected HashMap> initHolidayFuncs() { + + HashMap> holidays = new HashMap<>(super.initHolidayFuncs()); + holidays.put("padres", SpanishHolidayParserConfiguration::fathersDay); + holidays.put("madres", SpanishHolidayParserConfiguration::mothersDay); + holidays.put("acciondegracias", SpanishHolidayParserConfiguration::thanksgivingDay); + holidays.put("trabajador", SpanishHolidayParserConfiguration::internationalWorkersDay); + holidays.put("delaraza", SpanishHolidayParserConfiguration::columbusDay); + holidays.put("memoria", SpanishHolidayParserConfiguration::memorialDay); + holidays.put("pascuas", SpanishHolidayParserConfiguration::pascuas); + holidays.put("navidad", SpanishHolidayParserConfiguration::christmasDay); + holidays.put("nochebuena", SpanishHolidayParserConfiguration::christmasEve); + holidays.put("añonuevo", SpanishHolidayParserConfiguration::newYear); + holidays.put("nochevieja", SpanishHolidayParserConfiguration::newYearEve); + holidays.put("yuandan", SpanishHolidayParserConfiguration::newYear); + holidays.put("maestro", SpanishHolidayParserConfiguration::teacherDay); + holidays.put("todoslossantos", SpanishHolidayParserConfiguration::halloweenDay); + holidays.put("niño", SpanishHolidayParserConfiguration::childrenDay); + holidays.put("mujer", SpanishHolidayParserConfiguration::femaleDay); + + return holidays; + } + + private static LocalDateTime newYear(int year) { + return DateUtil.safeCreateFromMinValue(year, 1, 1); + } + + private static LocalDateTime newYearEve(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 31); + } + + private static LocalDateTime christmasDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 25); + } + + private static LocalDateTime christmasEve(int year) { + return DateUtil.safeCreateFromMinValue(year, 12, 24); + } + + private static LocalDateTime femaleDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 3, 8); + } + + private static LocalDateTime childrenDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 6, 1); + } + + private static LocalDateTime halloweenDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 10, 31); + } + + private static LocalDateTime teacherDay(int year) { + return DateUtil.safeCreateFromMinValue(year, 9, 10); + } + + private static LocalDateTime pascuas(int year) { + return HolidayFunctions.calculateHolidayByEaster(year); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishMergedParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishMergedParserConfiguration.java new file mode 100644 index 000000000..3c5ea9e4e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishMergedParserConfiguration.java @@ -0,0 +1,76 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.datetime.parsers.BaseHolidayParser; +import com.microsoft.recognizers.text.datetime.parsers.BaseSetParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.IMergedParserConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishDatePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishMergedExtractorConfiguration; +import com.microsoft.recognizers.text.matcher.StringMatcher; + +import java.util.regex.Pattern; + +public class SpanishMergedParserConfiguration extends SpanishCommonDateTimeParserConfiguration implements IMergedParserConfiguration { + + public SpanishMergedParserConfiguration(DateTimeOptions options) { + super(options); + + beforeRegex = SpanishMergedExtractorConfiguration.BeforeRegex; + afterRegex = SpanishMergedExtractorConfiguration.AfterRegex; + sinceRegex = SpanishMergedExtractorConfiguration.SinceRegex; + aroundRegex = SpanishMergedExtractorConfiguration.AroundRegex; + suffixAfterRegex = SpanishMergedExtractorConfiguration.SuffixAfterRegex; + yearRegex = SpanishDatePeriodExtractorConfiguration.YearRegex; + superfluousWordMatcher = SpanishMergedExtractorConfiguration.SuperfluousWordMatcher; + + getParser = new BaseSetParser(new SpanishSetParserConfiguration(this)); + holidayParser = new BaseHolidayParser(new SpanishHolidayParserConfiguration()); + } + + private final Pattern beforeRegex; + private final Pattern afterRegex; + private final Pattern sinceRegex; + private final Pattern aroundRegex; + private final Pattern suffixAfterRegex; + private final Pattern yearRegex; + private final IDateTimeParser getParser; + private final IDateTimeParser holidayParser; + private final StringMatcher superfluousWordMatcher; + + public Pattern getBeforeRegex() { + return beforeRegex; + } + + public Pattern getAfterRegex() { + return afterRegex; + } + + public Pattern getSinceRegex() { + return sinceRegex; + } + + public Pattern getAroundRegex() { + return aroundRegex; + } + + public Pattern getSuffixAfterRegex() { + return suffixAfterRegex; + } + + public Pattern getYearRegex() { + return yearRegex; + } + + public IDateTimeParser getGetParser() { + return getParser; + } + + public IDateTimeParser getHolidayParser() { + return holidayParser; + } + + public StringMatcher getSuperfluousWordMatcher() { + return superfluousWordMatcher; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishSetParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishSetParserConfiguration.java new file mode 100644 index 000000000..9cf688cb2 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishSetParserConfiguration.java @@ -0,0 +1,218 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateExtractor; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ISetParserConfiguration; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishSetExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.MatchedTimexResult; + +import java.util.Locale; +import java.util.regex.Pattern; + +public class SpanishSetParserConfiguration extends BaseOptionsConfiguration implements ISetParserConfiguration { + + private IDateTimeExtractor durationExtractor; + + public final IDateTimeExtractor getDurationExtractor() { + return durationExtractor; + } + + private IDateTimeParser durationParser; + + public final IDateTimeParser getDurationParser() { + return durationParser; + } + + private IDateTimeExtractor timeExtractor; + + public final IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + private IDateTimeParser timeParser; + + public final IDateTimeParser getTimeParser() { + return timeParser; + } + + private IDateExtractor dateExtractor; + + public final IDateExtractor getDateExtractor() { + return dateExtractor; + } + + private IDateTimeParser dateParser; + + public final IDateTimeParser getDateParser() { + return dateParser; + } + + private IDateTimeExtractor dateTimeExtractor; + + public final IDateTimeExtractor getDateTimeExtractor() { + return dateTimeExtractor; + } + + private IDateTimeParser dateTimeParser; + + public final IDateTimeParser getDateTimeParser() { + return dateTimeParser; + } + + private IDateTimeExtractor datePeriodExtractor; + + public final IDateTimeExtractor getDatePeriodExtractor() { + return datePeriodExtractor; + } + + private IDateTimeParser datePeriodParser; + + public final IDateTimeParser getDatePeriodParser() { + return datePeriodParser; + } + + private IDateTimeExtractor timePeriodExtractor; + + public final IDateTimeExtractor getTimePeriodExtractor() { + return timePeriodExtractor; + } + + private IDateTimeParser timePeriodParser; + + public final IDateTimeParser getTimePeriodParser() { + return timePeriodParser; + } + + private IDateTimeExtractor dateTimePeriodExtractor; + + public final IDateTimeExtractor getDateTimePeriodExtractor() { + return dateTimePeriodExtractor; + } + + private IDateTimeParser dateTimePeriodParser; + + public final IDateTimeParser getDateTimePeriodParser() { + return dateTimePeriodParser; + } + + private ImmutableMap unitMap; + + public final ImmutableMap getUnitMap() { + return unitMap; + } + + private Pattern eachPrefixRegex; + + public final Pattern getEachPrefixRegex() { + return eachPrefixRegex; + } + + private Pattern periodicRegex; + + public final Pattern getPeriodicRegex() { + return periodicRegex; + } + + private Pattern eachUnitRegex; + + public final Pattern getEachUnitRegex() { + return eachUnitRegex; + } + + private Pattern eachDayRegex; + + public final Pattern getEachDayRegex() { + return eachDayRegex; + } + + private Pattern setWeekDayRegex; + + public final Pattern getSetWeekDayRegex() { + return setWeekDayRegex; + } + + private Pattern setEachRegex; + + public final Pattern getSetEachRegex() { + return setEachRegex; + } + + public SpanishSetParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + durationExtractor = config.getDurationExtractor(); + timeExtractor = config.getTimeExtractor(); + dateExtractor = config.getDateExtractor(); + dateTimeExtractor = config.getDateTimeExtractor(); + datePeriodExtractor = config.getDatePeriodExtractor(); + timePeriodExtractor = config.getTimePeriodExtractor(); + dateTimePeriodExtractor = config.getDateTimePeriodExtractor(); + + durationParser = config.getDurationParser(); + timeParser = config.getTimeParser(); + dateParser = config.getDateParser(); + dateTimeParser = config.getDateTimeParser(); + datePeriodParser = config.getDatePeriodParser(); + timePeriodParser = config.getTimePeriodParser(); + dateTimePeriodParser = config.getDateTimePeriodParser(); + unitMap = config.getUnitMap(); + + eachPrefixRegex = SpanishSetExtractorConfiguration.EachPrefixRegex; + periodicRegex = SpanishSetExtractorConfiguration.PeriodicRegex; + eachUnitRegex = SpanishSetExtractorConfiguration.EachUnitRegex; + eachDayRegex = SpanishSetExtractorConfiguration.EachDayRegex; + setWeekDayRegex = SpanishSetExtractorConfiguration.SetWeekDayRegex; + setEachRegex = SpanishSetExtractorConfiguration.SetEachRegex; + } + + public MatchedTimexResult getMatchedDailyTimex(String text) { + + MatchedTimexResult result = new MatchedTimexResult(); + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + + if (trimmedText.endsWith("diario") || trimmedText.endsWith("diariamente")) { + result.setTimex("P1D"); + } else if (trimmedText.equals("semanalmente")) { + result.setTimex("P1W"); + } else if (trimmedText.equals("quincenalmente")) { + result.setTimex("P2W"); + } else if (trimmedText.equals("mensualmente")) { + result.setTimex("P1M"); + } else if (trimmedText.equals("anualmente")) { + result.setTimex("P1Y"); + } + + if (result.getTimex() != "") { + result.setResult(true); + } + + return result; + } + + public MatchedTimexResult getMatchedUnitTimex(String text) { + + MatchedTimexResult result = new MatchedTimexResult(); + String trimmedText = text.trim().toLowerCase(Locale.ROOT); + + if (trimmedText.equals("día") || trimmedText.equals("dia") || trimmedText.equals("días") || trimmedText.equals("dias")) { + result.setTimex("P1D"); + } else if (trimmedText.equals("semana") || trimmedText.equals("semanas")) { + result.setTimex("P1W"); + } else if (trimmedText.equals("mes") || trimmedText.equals("meses")) { + result.setTimex("P1M"); + } else if (trimmedText.equals("año") || trimmedText.equals("años")) { + result.setTimex("P1Y"); + } + + if (result.getTimex() != "") { + result.setResult(true); + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishTimeParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishTimeParserConfiguration.java new file mode 100644 index 000000000..31e7b56ad --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishTimeParserConfiguration.java @@ -0,0 +1,161 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.BaseTimeZoneParser; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.PrefixAdjustResult; +import com.microsoft.recognizers.text.datetime.parsers.config.SuffixAdjustResult; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishTimeExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.RegexExtension; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public class SpanishTimeParserConfiguration extends BaseOptionsConfiguration implements ITimeParserConfiguration { + + public String timeTokenPrefix = SpanishDateTime.TimeTokenPrefix; + + public final Pattern atRegex; + public Pattern mealTimeRegex; + + private final Iterable timeRegexes; + private final ImmutableMap numbers; + private final IDateTimeUtilityConfiguration utilityConfiguration; + private final IDateTimeParser timeZoneParser; + + public SpanishTimeParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + timeZoneParser = new BaseTimeZoneParser(); + + atRegex = SpanishTimeExtractorConfiguration.AtRegex; + timeRegexes = SpanishTimeExtractorConfiguration.TimeRegexList; + } + + @Override + public String getTimeTokenPrefix() { + return timeTokenPrefix; + } + + @Override + public Pattern getAtRegex() { + return atRegex; + } + + @Override + public Iterable getTimeRegexes() { + return timeRegexes; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public PrefixAdjustResult adjustByPrefix(String prefix, int hour, int min, boolean hasMin) { + + int deltaMin = 0; + String trimmedPrefix = prefix.trim().toLowerCase(); + + if (trimmedPrefix.startsWith("cuarto") || trimmedPrefix.startsWith("y cuarto")) { + deltaMin = 15; + } else if (trimmedPrefix.startsWith("menos cuarto")) { + deltaMin = -15; + } else if (trimmedPrefix.startsWith("media") || trimmedPrefix.startsWith("y media")) { + deltaMin = 30; + } else { + Optional match = Arrays.stream(RegExpUtility.getMatches(SpanishTimeExtractorConfiguration.LessThanOneHour, trimmedPrefix)).findFirst(); + if (match.isPresent()) { + String minStr = match.get().getGroup("deltamin").value; + if (!StringUtility.isNullOrWhiteSpace(minStr)) { + deltaMin = Integer.parseInt(minStr); + } else { + minStr = match.get().getGroup("deltaminnum").value.toLowerCase(); + deltaMin = numbers.getOrDefault(minStr, 0); + } + } + } + + if (trimmedPrefix.endsWith("pasadas") || trimmedPrefix.endsWith("pasados") || trimmedPrefix.endsWith("pasadas las") || + trimmedPrefix.endsWith("pasados las") || trimmedPrefix.endsWith("pasadas de las") || trimmedPrefix.endsWith("pasados de las")) { + //deltaMin is positive + } else if (trimmedPrefix.endsWith("para la") || trimmedPrefix.endsWith("para las") || + trimmedPrefix.endsWith("antes de la") || trimmedPrefix.endsWith("antes de las")) { + deltaMin = -deltaMin; + } + + min += deltaMin; + if (min < 0) { + min += 60; + hour -= 1; + } + + hasMin = hasMin || (min != 0); + + return new PrefixAdjustResult(hour, min, hasMin); + } + + @Override + public SuffixAdjustResult adjustBySuffix(String suffix, int hour, int min, boolean hasMin, boolean hasAm, boolean hasPm) { + + String trimmedSuffix = suffix.trim().toLowerCase(); + PrefixAdjustResult prefixAdjustResult = adjustByPrefix(trimmedSuffix,hour, min, hasMin); + hour = prefixAdjustResult.hour; + min = prefixAdjustResult.minute; + hasMin = prefixAdjustResult.hasMin; + + int deltaHour = 0; + ConditionalMatch match = RegexExtension.matchExact(SpanishTimeExtractorConfiguration.TimeSuffix, trimmedSuffix, true); + if (match.getSuccess()) { + + String oclockStr = match.getMatch().get().getGroup("oclock").value; + if (StringUtility.isNullOrEmpty(oclockStr)) { + + String amStr = match.getMatch().get().getGroup(Constants.AmGroupName).value; + if (!StringUtility.isNullOrEmpty(amStr)) { + if (hour >= Constants.HalfDayHourCount) { + deltaHour = -Constants.HalfDayHourCount; + } + hasAm = true; + } + + String pmStr = match.getMatch().get().getGroup(Constants.PmGroupName).value; + if (!StringUtility.isNullOrEmpty(pmStr)) { + if (hour < Constants.HalfDayHourCount) { + deltaHour = Constants.HalfDayHourCount; + } + hasPm = true; + } + } + } + + hour = (hour + deltaHour) % 24; + + return new SuffixAdjustResult(hour, min, hasMin, hasAm, hasPm); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishTimePeriodParserConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishTimePeriodParserConfiguration.java new file mode 100644 index 000000000..c4e8debcc --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/parsers/SpanishTimePeriodParserConfiguration.java @@ -0,0 +1,152 @@ +package com.microsoft.recognizers.text.datetime.spanish.parsers; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.IExtractor; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.config.BaseOptionsConfiguration; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.datetime.parsers.config.ICommonDateTimeParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.ITimePeriodParserConfiguration; +import com.microsoft.recognizers.text.datetime.parsers.config.MatchedTimeRangeResult; +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.spanish.extractors.SpanishTimePeriodExtractorConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.datetime.utilities.TimeOfDayResolutionResult; +import com.microsoft.recognizers.text.datetime.utilities.TimexUtility; + +import java.util.regex.Pattern; + +public class SpanishTimePeriodParserConfiguration extends BaseOptionsConfiguration implements ITimePeriodParserConfiguration { + + private final IDateTimeExtractor timeExtractor; + private final IDateTimeParser timeParser; + private final IExtractor integerExtractor; + private final IDateTimeParser timeZoneParser; + + private final Pattern pureNumberFromToRegex; + private final Pattern pureNumberBetweenAndRegex; + private final Pattern specificTimeFromToRegex; + private final Pattern specificTimeBetweenAndRegex; + private final Pattern timeOfDayRegex; + private final Pattern generalEndingRegex; + private final Pattern tillRegex; + + private final ImmutableMap numbers; + private final IDateTimeUtilityConfiguration utilityConfiguration; + + public SpanishTimePeriodParserConfiguration(ICommonDateTimeParserConfiguration config) { + + super(config.getOptions()); + + timeExtractor = config.getTimeExtractor(); + integerExtractor = config.getIntegerExtractor(); + timeParser = config.getTimeParser(); + timeZoneParser = config.getTimeZoneParser(); + pureNumberFromToRegex = SpanishTimePeriodExtractorConfiguration.PureNumFromTo; + pureNumberBetweenAndRegex = SpanishTimePeriodExtractorConfiguration.PureNumBetweenAnd; + specificTimeFromToRegex = SpanishTimePeriodExtractorConfiguration.SpecificTimeFromTo; + specificTimeBetweenAndRegex = SpanishTimePeriodExtractorConfiguration.SpecificTimeBetweenAnd; + timeOfDayRegex = SpanishTimePeriodExtractorConfiguration.TimeOfDayRegex; + generalEndingRegex = SpanishTimePeriodExtractorConfiguration.GeneralEndingRegex; + tillRegex = SpanishTimePeriodExtractorConfiguration.TillRegex; + numbers = config.getNumbers(); + utilityConfiguration = config.getUtilityConfiguration(); + } + + @Override + public IDateTimeExtractor getTimeExtractor() { + return timeExtractor; + } + + @Override + public IDateTimeParser getTimeParser() { + return timeParser; + } + + @Override + public IExtractor getIntegerExtractor() { + return integerExtractor; + } + + @Override + public IDateTimeParser getTimeZoneParser() { + return timeZoneParser; + } + + @Override + public Pattern getPureNumberFromToRegex() { + return pureNumberFromToRegex; + } + + @Override + public Pattern getPureNumberBetweenAndRegex() { + return pureNumberBetweenAndRegex; + } + + @Override + public Pattern getSpecificTimeFromToRegex() { + return specificTimeFromToRegex; + } + + @Override + public Pattern getSpecificTimeBetweenAndRegex() { + return specificTimeBetweenAndRegex; + } + + @Override + public Pattern getTimeOfDayRegex() { + return timeOfDayRegex; + } + + @Override + public Pattern getGeneralEndingRegex() { + return generalEndingRegex; + } + + @Override + public Pattern getTillRegex() { + return tillRegex; + } + + @Override + public ImmutableMap getNumbers() { + return numbers; + } + + @Override + public IDateTimeUtilityConfiguration getUtilityConfiguration() { + return utilityConfiguration; + } + + @Override + public MatchedTimeRangeResult getMatchedTimexRange(String text, String timex, int beginHour, int endHour, int endMin) { + + String trimmedText = text.trim().toLowerCase(); + + beginHour = 0; + endHour = 0; + endMin = 0; + + String timeOfDay = ""; + + if (SpanishDateTime.EarlyMorningTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.EarlyMorning; + } else if (SpanishDateTime.MorningTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Morning; + } else if (SpanishDateTime.AfternoonTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Afternoon; + } else if (SpanishDateTime.EveningTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Evening; + } else if (SpanishDateTime.NightTermList.stream().anyMatch(trimmedText::endsWith)) { + timeOfDay = Constants.Night; + } else { + timex = null; + return new MatchedTimeRangeResult(false, timex, beginHour, endHour, endMin); + } + + TimeOfDayResolutionResult result = TimexUtility.parseTimeOfDay(timeOfDay); + + return new MatchedTimeRangeResult(true, result.getTimex(), result.getBeginHour(), result.getEndHour(), result.getEndMin()); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/utilities/SpanishDatetimeUtilityConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/utilities/SpanishDatetimeUtilityConfiguration.java new file mode 100644 index 000000000..874d5e904 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/spanish/utilities/SpanishDatetimeUtilityConfiguration.java @@ -0,0 +1,86 @@ +package com.microsoft.recognizers.text.datetime.spanish.utilities; + +import com.microsoft.recognizers.text.datetime.resources.SpanishDateTime; +import com.microsoft.recognizers.text.datetime.utilities.IDateTimeUtilityConfiguration; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.regex.Pattern; + +public class SpanishDatetimeUtilityConfiguration implements IDateTimeUtilityConfiguration { + public static final Pattern AgoRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AgoRegex); + + public static final Pattern LaterRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.LaterRegex); + + public static final Pattern InConnectorRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.InConnectorRegex); + + public static final Pattern WithinNextPrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.WithinNextPrefixRegex); + + public static final Pattern AmDescRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AmDescRegex); + + public static final Pattern PmDescRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.PmDescRegex); + + public static final Pattern AmPmDescRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.AmPmDescRegex); + + public static final Pattern RangeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.RangeUnitRegex); + + public static final Pattern TimeUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.TimeUnitRegex); + + public static final Pattern DateUnitRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.DateUnitRegex); + + public static final Pattern CommonDatePrefixRegex = RegExpUtility.getSafeRegExp(SpanishDateTime.CommonDatePrefixRegex); + + @Override + public final Pattern getLaterRegex() { + return LaterRegex; + } + + @Override + public final Pattern getAgoRegex() { + return AgoRegex; + } + + @Override + public final Pattern getInConnectorRegex() { + return InConnectorRegex; + } + + @Override + public final Pattern getWithinNextPrefixRegex() { + return WithinNextPrefixRegex; + } + + @Override + public final Pattern getAmDescRegex() { + return AmDescRegex; + } + + @Override + public final Pattern getPmDescRegex() { + return PmDescRegex; + } + + @Override + public final Pattern getAmPmDescRegex() { + return AmPmDescRegex; + } + + @Override + public final Pattern getRangeUnitRegex() { + return RangeUnitRegex; + } + + @Override + public final Pattern getTimeUnitRegex() { + return TimeUnitRegex; + } + + @Override + public final Pattern getDateUnitRegex() { + return DateUnitRegex; + } + + @Override + public final Pattern getCommonDatePrefixRegex() { + return CommonDatePrefixRegex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/AgoLaterUtil.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/AgoLaterUtil.java new file mode 100644 index 000000000..2cff56be6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/AgoLaterUtil.java @@ -0,0 +1,197 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.extractors.IDateTimeExtractor; +import com.microsoft.recognizers.text.datetime.parsers.DateTimeParseResult; +import com.microsoft.recognizers.text.datetime.parsers.IDateTimeParser; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.MatchGroup; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class AgoLaterUtil { + public static DateTimeResolutionResult parseDurationWithAgoAndLater(String text, LocalDateTime referenceTime, + IDateTimeExtractor durationExtractor, IDateTimeParser durationParser, ImmutableMap unitMap, + Pattern unitRegex, IDateTimeUtilityConfiguration utilityConfiguration, + Function getSwiftDay) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + List durationRes = durationExtractor.extract(text, referenceTime); + if (durationRes.size() > 0) { + DateTimeParseResult pr = durationParser.parse(durationRes.get(0), referenceTime); + Match[] matches = RegExpUtility.getMatches(unitRegex, text); + if (matches.length > 0) { + String afterStr = text.substring(durationRes.get(0).getStart() + durationRes.get(0).getLength()).trim() + .toLowerCase(); + + String beforeStr = text.substring(0, durationRes.get(0).getStart()).trim().toLowerCase(); + + AgoLaterMode mode = AgoLaterMode.DATE; + if (pr.getTimexStr().contains("T")) { + mode = AgoLaterMode.DATETIME; + } + + if (pr.getValue() != null) { + return getAgoLaterResult(pr, afterStr, beforeStr, referenceTime, utilityConfiguration, mode, + getSwiftDay); + } + } + } + return ret; + } + + private static DateTimeResolutionResult getAgoLaterResult(DateTimeParseResult durationParseResult, String afterStr, + String beforeStr, LocalDateTime referenceTime, IDateTimeUtilityConfiguration utilityConfiguration, + AgoLaterMode mode, Function getSwiftDay) { + DateTimeResolutionResult ret = new DateTimeResolutionResult(); + LocalDateTime resultDateTime = referenceTime; + String timex = durationParseResult.getTimexStr(); + + if (((DateTimeResolutionResult)durationParseResult.getValue()).getMod() == Constants.MORE_THAN_MOD) { + ret.setMod(Constants.MORE_THAN_MOD); + } else if (((DateTimeResolutionResult)durationParseResult.getValue()).getMod() == Constants.LESS_THAN_MOD) { + ret.setMod(Constants.LESS_THAN_MOD); + } + + if (MatchingUtil.containsAgoLaterIndex(afterStr, utilityConfiguration.getAgoRegex())) { + Optional match = Arrays + .stream(RegExpUtility.getMatches(utilityConfiguration.getAgoRegex(), afterStr)).findFirst(); + int swift = 0; + + // Handle cases like "3 days before yesterday" + if (match.isPresent() && !StringUtility.isNullOrEmpty(match.get().getGroup("day").value)) { + swift = getSwiftDay.apply(match.get().getGroup("day").value); + } + + resultDateTime = DurationParsingUtil.shiftDateTime(timex, referenceTime.plusDays(swift), false); + + ((DateTimeResolutionResult)durationParseResult.getValue()).setMod(Constants.BEFORE_MOD); + } else if (MatchingUtil.containsAgoLaterIndex(afterStr, utilityConfiguration.getLaterRegex()) || + MatchingUtil.containsTermIndex(beforeStr, utilityConfiguration.getInConnectorRegex())) { + Optional match = Arrays + .stream(RegExpUtility.getMatches(utilityConfiguration.getLaterRegex(), afterStr)).findFirst(); + int swift = 0; + + // Handle cases like "3 days after tomorrow" + if (match.isPresent() && !StringUtility.isNullOrEmpty(match.get().getGroup("day").value)) { + swift = getSwiftDay.apply(match.get().getGroup("day").value); + } + + resultDateTime = DurationParsingUtil.shiftDateTime(timex, referenceTime.plusDays(swift), true); + + ((DateTimeResolutionResult)durationParseResult.getValue()).setMod(Constants.AFTER_MOD); + } + + if (resultDateTime != referenceTime) { + if (mode.equals(AgoLaterMode.DATE)) { + ret.setTimex(DateTimeFormatUtil.luisDate(resultDateTime)); + } else if (mode.equals(AgoLaterMode.DATETIME)) { + ret.setTimex(DateTimeFormatUtil.luisDateTime(resultDateTime)); + } + + ret.setFutureValue(resultDateTime); + ret.setPastValue(resultDateTime); + + List subDateTimeEntities = new ArrayList<>(); + subDateTimeEntities.add(durationParseResult); + + ret.setSubDateTimeEntities(subDateTimeEntities); + ret.setSuccess(true); + } + + return ret; + } + + public static List extractorDurationWithBeforeAndAfter(String text, ExtractResult er, List result, + IDateTimeUtilityConfiguration utilityConfiguration) { + int pos = er.getStart() + er.getLength(); + if (pos <= text.length()) { + String afterString = text.substring(pos); + String beforeString = text.substring(0, er.getStart()); + boolean isTimeDuration = RegExpUtility.getMatches(utilityConfiguration.getTimeUnitRegex(), + er.getText()).length != 0; + + MatchingUtilResult resultIndex = MatchingUtil.getAgoLaterIndex(afterString, + utilityConfiguration.getAgoRegex()); + if (resultIndex.result) { + // We don't support cases like "5 minutes from today" for now + // Cases like "5 minutes ago" or "5 minutes from now" are supported + // Cases like "2 days before today" or "2 weeks from today" are also supported + Optional match = Arrays.stream(RegExpUtility.getMatches(utilityConfiguration.getAgoRegex(), afterString)).findFirst(); + boolean isDayMatchInAfterString = match.isPresent() && !match.get().getGroup("day").value.equals(""); + + if (!(isTimeDuration && isDayMatchInAfterString)) { + result.add(new Token(er.getStart(), er.getStart() + er.getLength() + resultIndex.index)); + } + } else { + resultIndex = MatchingUtil.getAgoLaterIndex(afterString, utilityConfiguration.getLaterRegex()); + if (resultIndex.result) { + Optional match = Arrays.stream(RegExpUtility.getMatches(utilityConfiguration.getLaterRegex(), afterString)).findFirst(); + boolean isDayMatchInAfterString = match.isPresent() && !match.get().getGroup("day").value.equals(""); + + if (!(isTimeDuration && isDayMatchInAfterString)) { + result.add(new Token(er.getStart(), er.getStart() + er.getLength() + resultIndex.index)); + } + } else { + resultIndex = MatchingUtil.getTermIndex(beforeString, utilityConfiguration.getInConnectorRegex()); + if (resultIndex.result) { + // For range unit like "week, month, year", it should output dateRange or + // datetimeRange + Optional match = Arrays + .stream(RegExpUtility.getMatches(utilityConfiguration.getRangeUnitRegex(), er.getText())) + .findFirst(); + if (!match.isPresent()) { + if (er.getStart() >= resultIndex.index) { + result.add(new Token(er.getStart() - resultIndex.index, er.getStart() + er.getLength())); + } + } + } else { + resultIndex = MatchingUtil.getTermIndex(beforeString, + utilityConfiguration.getWithinNextPrefixRegex()); + if (resultIndex.result) { + // For range unit like "week, month, year, day, second, minute, hour", it should + // output dateRange or datetimeRange + Optional matchDateUnitRegex = Arrays + .stream(RegExpUtility.getMatches(utilityConfiguration.getDateUnitRegex(), er.getText())) + .findFirst(); + Optional matchTimeUnitRegex = Arrays + .stream(RegExpUtility.getMatches(utilityConfiguration.getTimeUnitRegex(), er.getText())) + .findFirst(); + if (!matchDateUnitRegex.isPresent() && !matchTimeUnitRegex.isPresent()) { + if (er.getStart() >= resultIndex.index) { + result.add(new Token(er.getStart() - resultIndex.index, er.getStart() + er.getLength())); + } + } + } + } + } + } + } + + return result; + } + + private static boolean isDayMatchInAfterString(String text, Pattern pattern, String group) { + Optional match = Arrays.stream(RegExpUtility.getMatches(pattern, text)).findFirst(); + + if (match.isPresent()) { + MatchGroup matchGroup = match.get().getGroup(group); + return !StringUtility.isNullOrEmpty(matchGroup.value); + } + + return false; + } + + public enum AgoLaterMode { + DATE, DATETIME + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/ConditionalMatch.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/ConditionalMatch.java new file mode 100644 index 000000000..2fe1304c5 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/ConditionalMatch.java @@ -0,0 +1,27 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.utilities.Match; + +import java.util.Optional; + +public class ConditionalMatch { + + private final Optional match; + private final boolean success; + + public ConditionalMatch(Optional match, boolean success) { + this.match = match; + this.success = success; + } + + public Optional getMatch() { + + return match; + } + + public boolean getSuccess() { + + return success; + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateContext.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateContext.java new file mode 100644 index 000000000..56e1a9ed6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateContext.java @@ -0,0 +1,165 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.parsers.DateTimeParseResult; +import java.time.LocalDateTime; +import java.util.HashMap; + +import org.javatuples.Pair; + +// Currently only Year is enabled as context, we may support Month or Week in the future +public class DateContext { + private int year = Constants.InvalidYear; + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public DateTimeParseResult processDateEntityParsingResult(DateTimeParseResult originalResult) { + if (!isEmpty()) { + originalResult.setTimexStr(TimexUtility.setTimexWithContext(originalResult.getTimexStr(), this)); + originalResult.setValue(processDateEntityResolution((DateTimeResolutionResult)originalResult.getValue())); + } + + return originalResult; + } + + public DateTimeResolutionResult processDateEntityResolution(DateTimeResolutionResult resolutionResult) { + if (!isEmpty()) { + resolutionResult.setTimex(TimexUtility.setTimexWithContext(resolutionResult.getTimex(), this)); + resolutionResult.setFutureValue(setDateWithContext((LocalDateTime)resolutionResult.getFutureValue())); + resolutionResult.setPastValue(setDateWithContext((LocalDateTime)resolutionResult.getPastValue())); + } + + return resolutionResult; + } + + public DateTimeResolutionResult processDatePeriodEntityResolution(DateTimeResolutionResult resolutionResult) { + if (!isEmpty()) { + resolutionResult.setTimex(TimexUtility.setTimexWithContext(resolutionResult.getTimex(), this)); + resolutionResult.setFutureValue(setDateRangeWithContext((Pair)resolutionResult.getFutureValue())); + resolutionResult.setPastValue(setDateRangeWithContext((Pair)resolutionResult.getPastValue())); + } + + return resolutionResult; + } + + // Generate future/past date for cases without specific year like "Feb 29th" + public static HashMap generateDates(boolean noYear, LocalDateTime referenceDate, int year, int month, int day) { + HashMap result = new HashMap<>(); + LocalDateTime futureDate = DateUtil.safeCreateFromMinValue(year, month, day); + LocalDateTime pastDate = DateUtil.safeCreateFromMinValue(year, month, day); + int futureYear = year; + int pastYear = year; + if (noYear) { + if (isFeb29th(year, month, day)) { + if (isLeapYear(year)) { + if (futureDate.compareTo(referenceDate) < 0) { + futureDate = DateUtil.safeCreateFromMinValue(futureYear + 4, month, day); + } else { + pastDate = DateUtil.safeCreateFromMinValue(pastYear - 4, month, day); + } + } else { + pastYear = pastYear >> 2 << 2; + if (!isLeapYear(pastYear)) { + pastYear -= 4; + } + + futureYear = pastYear + 4; + if (!isLeapYear(futureYear)) { + futureYear += 4; + } + futureDate = DateUtil.safeCreateFromMinValue(futureYear, month, day); + pastDate = DateUtil.safeCreateFromMinValue(pastYear, month, day); + } + } else { + if (futureDate.compareTo(referenceDate) < 0 && DateUtil.isValidDate(year, month, day)) { + futureDate = DateUtil.safeCreateFromMinValue(year + 1, month, day); + } + + if (pastDate.compareTo(referenceDate) >= 0 && DateUtil.isValidDate(year, month, day)) { + pastDate = DateUtil.safeCreateFromMinValue(year - 1, month, day); + } + } + } + result.put(Constants.FutureDate, futureDate); + result.put(Constants.PastDate, pastDate); + return result; + } + + private static boolean isLeapYear(int year) { + return (((year % 4) == 0) && (((year % 100) != 0) || ((year % 400) == 0))); + } + + private static boolean isFeb29th(int year, int month, int day) { + return month == 2 && day == 29; + } + + public static boolean isFeb29th(LocalDateTime date) { + return date.getMonthValue() == 2 && date.getDayOfMonth() == 29; + } + + // This method is to ensure the year of begin date is same with the end date in no year situation. + public HashMap syncYear(DateTimeParseResult pr1, DateTimeParseResult pr2) { + if (this.isEmpty()) { + int futureYear; + int pastYear; + if (isFeb29th((LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getFutureValue())) { + futureYear = ((LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getFutureValue()).getYear(); + pastYear = ((LocalDateTime)((DateTimeResolutionResult)pr1.getValue()).getPastValue()).getYear(); + pr2.setValue(syncYearResolution((DateTimeResolutionResult)pr2.getValue(), futureYear, pastYear)); + } else if (isFeb29th((LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getFutureValue())) { + futureYear = ((LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getFutureValue()).getYear(); + pastYear = ((LocalDateTime)((DateTimeResolutionResult)pr2.getValue()).getPastValue()).getYear(); + pr1.setValue(syncYearResolution((DateTimeResolutionResult)pr1.getValue(), futureYear, pastYear)); + } + } + + HashMap result = new HashMap<>(); + result.put(Constants.ParseResult1, pr1); + result.put(Constants.ParseResult2, pr2); + return result; + } + + public DateTimeResolutionResult syncYearResolution(DateTimeResolutionResult resolutionResult, int futureYear, int pastYear) { + resolutionResult.setFutureValue(setDateWithContext((LocalDateTime)resolutionResult.getFutureValue(), futureYear)); + resolutionResult.setPastValue(setDateWithContext((LocalDateTime)resolutionResult.getPastValue(), pastYear)); + return resolutionResult; + } + + public boolean isEmpty() { + return this.year == Constants.InvalidYear; + } + + // This method is to ensure the begin date is less than the end date + // As DateContext only support common Year as context, so decrease year part of begin date to ensure the begin date is less than end date + public LocalDateTime swiftDateObject(LocalDateTime beginDate, LocalDateTime endDate) { + if (beginDate.isAfter(endDate)) { + beginDate = beginDate.plusYears(-1); + } + + return beginDate; + } + + private LocalDateTime setDateWithContext(LocalDateTime originalDate) { + return setDateWithContext(originalDate, this.year); + } + + private LocalDateTime setDateWithContext(LocalDateTime originalDate, int year) { + if (!DateUtil.isDefaultValue(originalDate)) { + return DateUtil.safeCreateFromMinValue(year, originalDate.getMonthValue(), originalDate.getDayOfMonth()); + } + return originalDate; + } + + private Pair setDateRangeWithContext(Pair originalDateRange) { + LocalDateTime startDate = setDateWithContext(originalDateRange.getValue0()); + LocalDateTime endDate = setDateWithContext(originalDateRange.getValue1()); + + return new Pair<>(startDate, endDate); + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateTimeFormatUtil.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateTimeFormatUtil.java new file mode 100644 index 000000000..f6b194402 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateTimeFormatUtil.java @@ -0,0 +1,248 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.utilities.IntegerUtility; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.IsoFields; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DateTimeFormatUtil { + + private static final Pattern HourTimexRegex = Pattern.compile("(? 0) { + result = String.format("%s%sH", result, timeSpan.toHours() % 24); + } + + if (timeSpan.toMinutes() % 60 > 0) { + result = String.format("%s%sM", result, timeSpan.toMinutes() % 60); + } + + if (timeSpan.get(ChronoUnit.SECONDS) % 60 > 0) { + result = String.format("%s%sS", result, timeSpan.get(ChronoUnit.SECONDS) % 60); + } + + if (timeSpan.toMinutes() % 60 > 0) { + result = String.format("%s%sM", result, timeSpan.toMinutes() % 60); + } + + if (timeSpan.get(ChronoUnit.SECONDS) % 60 > 0) { + result = String.format("%s%sS", result, timeSpan.get(ChronoUnit.SECONDS) % 60); + } + + return timeSpan.toString(); + } + + public static String toPm(String timeStr) { + boolean hasT = false; + if (timeStr.startsWith("T")) { + hasT = true; + timeStr = timeStr.substring(1); + } + + String[] splited = timeStr.split(":"); + int hour = Integer.parseInt(splited[0]); + hour = hour >= Constants.HalfDayHourCount ? hour - Constants.HalfDayHourCount : hour + Constants.HalfDayHourCount; + splited[0] = String.format("%02d", hour); + timeStr = String.join(":", splited); + + return hasT ? "T" + timeStr : timeStr; + } + + public static String allStringToPm(String timeStr) { + Match[] matches = RegExpUtility.getMatches(HourTimexRegex, timeStr); + ArrayList splited = new ArrayList<>(); + + int lastPos = 0; + for (Match match : matches) { + if (lastPos != match.index) { + splited.add(timeStr.substring(lastPos, match.index)); + } + splited.add(timeStr.substring(match.index, match.index + match.length)); + lastPos = match.index + match.length; + } + + if (!StringUtility.isNullOrEmpty(timeStr.substring(lastPos))) { + splited.add(timeStr.substring(lastPos)); + } + + for (int i = 0; i < splited.size(); i++) { + if (HourTimexRegex.matcher(splited.get(i)).lookingAt()) { + splited.set(i, toPm(splited.get(i))); + } + } + + // Modify weekDay timex for the cases which cross day boundary + if (splited.size() >= 4) { + Matcher weekDayStartMatch = WeekDayTimexRegex.matcher(splited.get(0)); + Matcher weekDayEndMatch = WeekDayTimexRegex.matcher(splited.get(2)); + Matcher hourStartMatch = HourTimexRegex.matcher(splited.get(1)); + Matcher hourEndMatch = HourTimexRegex.matcher(splited.get(3)); + + String weekDayStartStr = weekDayStartMatch.find() ? weekDayStartMatch.group(1) : ""; + String weekDayEndStr = weekDayEndMatch.find() ? weekDayEndMatch.group(1) : ""; + String hourStartStr = hourStartMatch.find() ? hourStartMatch.group(1) : ""; + String hourEndStr = hourEndMatch.find() ? hourEndMatch.group(1) : ""; + + if (IntegerUtility.canParse(weekDayStartStr) && + IntegerUtility.canParse(weekDayEndStr) && + IntegerUtility.canParse(hourStartStr) && + IntegerUtility.canParse(hourEndStr)) { + int weekDayStart = Integer.parseInt(weekDayStartStr); + int weekDayEnd = Integer.parseInt(weekDayEndStr); + int hourStart = Integer.parseInt(hourStartStr); + int hourEnd = Integer.parseInt(hourEndStr); + + if (hourEnd < hourStart && weekDayStart == weekDayEnd) { + weekDayEnd = weekDayEnd == Constants.WeekDayCount ? 1 : weekDayEnd + 1; + splited.set(2, splited.get(2).substring(0, weekDayEndMatch.start(1)) + weekDayEnd); + } + } + } + + return String.join("", splited); + } + + public static String toIsoWeekTimex(LocalDateTime date) { + int weekNum = LocalDate.of(date.getYear(), date.getMonthValue(), date.getDayOfMonth()).get(IsoFields.WEEK_OF_WEEK_BASED_YEAR); + return String.format("%04d-W%02d", date.getYear(), weekNum); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateTimeResolutionResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateTimeResolutionResult.java new file mode 100644 index 000000000..943ecf02e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateTimeResolutionResult.java @@ -0,0 +1,134 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import java.util.List; +import java.util.Map; + +public class DateTimeResolutionResult { + + private Boolean success; + private String timex; + private Boolean isLunar; + private String mod; + private Boolean hasRangeChangingMod; + private String comment; + + private Map futureResolution; + private Map pastResolution; + + private Object futureValue; + private Object pastValue; + + private List subDateTimeEntities; + + private TimeZoneResolutionResult timeZoneResolution; + + private List list; + + public DateTimeResolutionResult() { + success = hasRangeChangingMod = false; + } + + public Boolean getSuccess() { + return this.success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + public String getTimex() { + return this.timex; + } + + public void setTimex(String timex) { + this.timex = timex; + } + + public Boolean getIsLunar() { + return this.isLunar; + } + + public void setIsLunar(Boolean isLunar) { + this.isLunar = isLunar; + } + + public String getMod() { + return this.mod; + } + + public void setMod(String mod) { + this.mod = mod; + } + + public Boolean getHasRangeChangingMod() { + return this.hasRangeChangingMod; + } + + public void setHasRangeChangingMod(Boolean hasRangeChangingMod) { + this.hasRangeChangingMod = hasRangeChangingMod; + } + + public String getComment() { + return this.comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Map getFutureResolution() { + return this.futureResolution; + } + + public void setFutureResolution(Map futureResolution) { + this.futureResolution = futureResolution; + } + + public Map getPastResolution() { + return this.pastResolution; + } + + public void setPastResolution(Map pastResolution) { + this.pastResolution = pastResolution; + } + + public Object getFutureValue() { + return this.futureValue; + } + + public void setFutureValue(Object futureValue) { + this.futureValue = futureValue; + } + + public Object getPastValue() { + return this.pastValue; + } + + public void setPastValue(Object pastValue) { + this.pastValue = pastValue; + } + + public List getSubDateTimeEntities() { + return this.subDateTimeEntities; + } + + public void setSubDateTimeEntities(List subDateTimeEntities) { + this.subDateTimeEntities = subDateTimeEntities; + } + + public TimeZoneResolutionResult getTimeZoneResolution() { + return this.timeZoneResolution; + } + + public void setTimeZoneResolution(TimeZoneResolutionResult timeZoneResolution) { + this.timeZoneResolution = timeZoneResolution; + } + + public List getList() { + return this.list; + } + + public void setList(List list) { + this.list = list; + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateUtil.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateUtil.java new file mode 100644 index 000000000..0e1a26c19 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DateUtil.java @@ -0,0 +1,156 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalField; +import java.time.temporal.WeekFields; +import java.util.Locale; + +public class DateUtil { + + public static LocalDateTime safeCreateFromValue(LocalDateTime datetime, int year, int month, int day, int hour, int minute, int second) { + if (isValidDate(year, month, day) && isValidTime(hour, minute, second)) { + datetime = safeCreateFromValue(datetime, year, month, day); + datetime = datetime.plusHours(hour - datetime.getHour()); + datetime = datetime.plusMinutes((minute - datetime.getMinute())); + datetime = datetime.plusSeconds(second - datetime.getSecond()); + } + + return datetime; + } + + public static LocalDateTime safeCreateFromValue(LocalDateTime datetime, int year, int month, int day) { + if (isValidDate(year, month, day)) { + datetime = datetime.plusYears(year - datetime.getYear()); + datetime = datetime.plusMonths((month - datetime.getMonthValue())); + datetime = datetime.plusDays(day - datetime.getDayOfMonth()); + } + + return datetime; + } + + public static LocalDateTime safeCreateFromMinValue(int year, int month, int day) { + return safeCreateFromValue(minValue(), year, month, day, 0, 0, 0); + } + + public static LocalDateTime safeCreateFromMinValue(int year, int month, int day, int hour, int minute, int second) { + return safeCreateFromValue(minValue(), year, month, day, hour, minute, second); + } + + public static LocalDateTime safeCreateFromMinValue(LocalDate date, LocalTime time) { + return safeCreateFromValue(minValue(), + date.getYear(), date.getMonthValue(), date.getDayOfMonth(), + time.getHour(), time.getMinute(), time.getSecond() + ); + } + + public static LocalDateTime minValue() { + return LocalDateTime.of(1, 1, 1, 0, 0, 0, 0); + } + + public static Boolean isValidDate(int year, int month, int day) { + if (year < 1 || year > 9999) { + return false; + } + + Integer[] validDays = { + 31, + year % 4 == 0 && year % 100 != 0 || year % 400 == 0 ? 29 : 28, + 31, + 30, + 31, + 30, + 31, + 31, + 30, + 31, + 30, + 31 + }; + + return month >= 1 && month <= 12 && day >= 1 && day <= validDays[month - 1]; + } + + public static boolean isValidTime(int hour, int minute, int second) { + return 0 <= hour && hour <= 23 && + 0 <= minute && minute <= 59 && + 0 <= second && second <= 59; + } + + public static boolean isDefaultValue(LocalDateTime date) { + return date.equals(DateUtil.minValue()); + } + + private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) + .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) + .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0) + .toFormatter(); + + public static LocalDateTime tryParse(String date) { + try { + return LocalDateTime.parse(date, DATE_TIME_FORMATTER); + } catch (DateTimeParseException ex) { + return null; + } + } + + public static LocalDateTime next(LocalDateTime from, int dayOfWeek) { + int start = from.getDayOfWeek().getValue(); + + if (start == 0) { + start = 7; + } + + if (dayOfWeek == 0) { + dayOfWeek = 7; + } + + return from.plusDays(dayOfWeek - start + 7); + } + + public static LocalDateTime thisDate(LocalDateTime from, int dayOfWeek) { + int start = from.getDayOfWeek().getValue(); + + if (start == 0) { + start = 7; + } + + if (dayOfWeek == 0) { + dayOfWeek = 7; + } + + return from.plusDays(dayOfWeek - start); + } + + public static LocalDateTime last(LocalDateTime from, int dayOfWeek) { + int start = from.getDayOfWeek().getValue(); + + if (start == 0) { + start = 7; + } + + if (dayOfWeek == 0) { + dayOfWeek = 7; + } + + return from.plusDays(dayOfWeek - start - 7); + } + + public static LocalDateTime plusPeriodInNanos(LocalDateTime reference, double period, ChronoUnit unit) { + long nanos = unit.getDuration().toNanos(); + return reference.plusNanos(Math.round(nanos * period)); + } + + public static int weekOfYear(LocalDateTime date) { + TemporalField woy = WeekFields.ISO.weekOfYear(); + return date.get(woy); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DurationParsingUtil.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DurationParsingUtil.java new file mode 100644 index 000000000..0b2a9e40a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/DurationParsingUtil.java @@ -0,0 +1,187 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.utilities.DoubleUtility; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +public class DurationParsingUtil { + public static boolean isTimeDurationUnit(String unitStr) { + boolean result = false; + switch (unitStr) { + case "H": + result = true; + break; + case "M": + result = true; + break; + case "S": + result = true; + break; + default: + break; + } + return result; + } + + public static boolean isMultipleDuration(String timex) { + ImmutableMap map = resolveDurationTimex(timex); + return map.size() > 1; + } + + public static boolean isDateDuration(String timex) { + ImmutableMap map = resolveDurationTimex(timex); + + for (String unit : map.keySet()) { + if (isTimeDurationUnit(unit)) { + return false; + } + } + + return true; + } + + public static LocalDateTime shiftDateTime(String timex, LocalDateTime reference, boolean future) { + ImmutableMap timexUnitMap = resolveDurationTimex(timex); + + return getShiftResult(timexUnitMap, reference, future); + } + + public static LocalDateTime getShiftResult(ImmutableMap timexUnitMap, LocalDateTime reference, boolean future) { + LocalDateTime result = reference; + int futureOrPast = future ? 1 : -1; + for (Map.Entry pair : timexUnitMap.entrySet()) { + String unit = pair.getKey(); + ChronoUnit chronoUnit; + Double number = pair.getValue(); + + switch (unit) { + case "H": + chronoUnit = ChronoUnit.HOURS; + break; + case "M": + chronoUnit = ChronoUnit.MINUTES; + break; + case "S": + chronoUnit = ChronoUnit.SECONDS; + break; + case Constants.TimexDay: + chronoUnit = ChronoUnit.DAYS; + break; + case Constants.TimexWeek: + chronoUnit = ChronoUnit.WEEKS; + break; + case Constants.TimexMonthFull: + chronoUnit = null; + result = result.plusMonths(Math.round(number * futureOrPast)); + break; + case Constants.TimexYear: + chronoUnit = null; + result = result.plusYears(Math.round(number * futureOrPast)); + break; + case Constants.TimexBusinessDay: + chronoUnit = null; + result = getNthBusinessDay(result, Math.round(number.floatValue()), future).result; + break; + + default: + return result; + } + if (chronoUnit != null) { + result = DateUtil.plusPeriodInNanos(result, number * futureOrPast, chronoUnit); + } + } + return result; + } + + public static NthBusinessDayResult getNthBusinessDay(LocalDateTime startDate, int number, boolean isFuture) { + LocalDateTime date = startDate; + List dateList = new ArrayList<>(); + dateList.add(date); + + for (int i = 0; i < number; i++) { + date = getNextBusinessDay(date, isFuture); + dateList.add(date); + } + + if (!isFuture) { + Collections.reverse(dateList); + } + + return new NthBusinessDayResult(date, dateList); + + } + + public static LocalDateTime getNextBusinessDay(LocalDateTime startDate) { + return getNextBusinessDay(startDate, true); + } + + // By design it currently does not take holidays into account + public static LocalDateTime getNextBusinessDay(LocalDateTime startDate, boolean isFuture) { + int dateIncrement = isFuture ? 1 : -1; + LocalDateTime date = startDate.plusDays(dateIncrement); + + while (date.getDayOfWeek().equals(DayOfWeek.SATURDAY) || date.getDayOfWeek().equals(DayOfWeek.SUNDAY)) { + date = date.plusDays(dateIncrement); + } + + return date; + } + + private static ImmutableMap resolveDurationTimex(String timex) { + Builder resultBuilder = ImmutableMap.builder(); + + // resolve duration timex, such as P21DT2H(21 days 2 hours) + String durationStr = timex.replace('P', '\0'); + int numberStart = 0; + boolean isTime = false; + + // Resolve business days + if (durationStr.endsWith(Constants.TimexBusinessDay)) { + if (DoubleUtility.canParse(durationStr.substring(0, durationStr.length() - 2))) { + + double numVal = Double.parseDouble(durationStr.substring(0, durationStr.length() - 2)); + resultBuilder.put(Constants.TimexBusinessDay, numVal); + } + + return resultBuilder.build(); + } + + for (int i = 0; i < durationStr.length(); i++) { + if (Character.isLetter(durationStr.charAt(i))) { + if (durationStr.charAt(i) == 'T') { + isTime = true; + } else { + String numStr = durationStr.substring(numberStart, i); + + try { + Double number = Double.parseDouble(numStr); + String srcTimexUnit = durationStr.substring(i, i + 1); + + if (!isTime && srcTimexUnit.equals("M")) { + srcTimexUnit = "MON"; + } + + resultBuilder.put(srcTimexUnit, number); + + } catch (NumberFormatException e) { + return resultBuilder.build(); + } + + } + numberStart = i + 1; + } + } + + return resultBuilder.build(); + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/GetModAndDateResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/GetModAndDateResult.java new file mode 100644 index 000000000..2e424c492 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/GetModAndDateResult.java @@ -0,0 +1,22 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import java.time.LocalDateTime; +import java.util.List; + +public class GetModAndDateResult { + public final LocalDateTime beginDate; + public final LocalDateTime endDate; + public final String mod; + public final List dateList; + + public GetModAndDateResult(LocalDateTime beginDate, LocalDateTime endDate, String mod, List dateList) { + this.beginDate = beginDate; + this.endDate = endDate; + this.mod = mod; + this.dateList = dateList; + } + + public GetModAndDateResult() { + this(null, null, "", null); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/HolidayFunctions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/HolidayFunctions.java new file mode 100644 index 000000000..260d816e4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/HolidayFunctions.java @@ -0,0 +1,31 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import java.time.LocalDateTime; + +public class HolidayFunctions { + + public static LocalDateTime calculateHolidayByEaster(int year) { + return calculateHolidayByEaster(year, 0); + } + + public static LocalDateTime calculateHolidayByEaster(int year, int days) { + + int day = 0; + int month = 3; + + int g = year % 19; + int c = year / 100; + int h = (c - (int)(c / 4) - (int)(((8 * c) + 13) / 25) + (19 * g) + 15) % 30; + int i = h - ((int)(h / 28) * (1 - ((int)(h / 28) * (int)(29 / (h + 1)) * (int)((21 - g) / 11)))); + + day = i - ((year + (int)(year / 4) + i + 2 - c + (int)(c / 4)) % 7) + 28; + + if (day > 31) { + month++; + day -= 31; + } + + return DateUtil.safeCreateFromMinValue(year, month, day).plusDays(days); + } + +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/IDateTimeUtilityConfiguration.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/IDateTimeUtilityConfiguration.java new file mode 100644 index 000000000..5ea05f65b --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/IDateTimeUtilityConfiguration.java @@ -0,0 +1,28 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import java.util.regex.Pattern; + +public interface IDateTimeUtilityConfiguration { + + Pattern getAgoRegex(); + + Pattern getLaterRegex(); + + Pattern getInConnectorRegex(); + + Pattern getWithinNextPrefixRegex(); + + Pattern getRangeUnitRegex(); + + Pattern getTimeUnitRegex(); + + Pattern getDateUnitRegex(); + + Pattern getAmDescRegex(); + + Pattern getPmDescRegex(); + + Pattern getAmPmDescRegex(); + + Pattern getCommonDatePrefixRegex(); +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchedTimexResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchedTimexResult.java new file mode 100644 index 000000000..6f9c0bd39 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchedTimexResult.java @@ -0,0 +1,31 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +public class MatchedTimexResult { + private boolean result; + private String timex; + + public MatchedTimexResult(boolean result, String timex) { + this.result = result; + this.timex = timex; + } + + public MatchedTimexResult() { + this(false, ""); + } + + public boolean getResult() { + return result; + } + + public String getTimex() { + return timex; + } + + public void setResult(boolean result) { + this.result = result; + } + + public void setTimex(String timex) { + this.timex = timex; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchingUtil.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchingUtil.java new file mode 100644 index 000000000..e700d2f1f --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchingUtil.java @@ -0,0 +1,106 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.extractors.config.ProcessedSuperfluousWords; +import com.microsoft.recognizers.text.matcher.MatchResult; +import com.microsoft.recognizers.text.matcher.StringMatcher; +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class MatchingUtil { + + public static MatchingUtilResult getAgoLaterIndex(String text, Pattern pattern) { + int index = -1; + ConditionalMatch match = RegexExtension.matchBegin(pattern, text, true); + + if (match.getSuccess()) { + index = match.getMatch().get().index + match.getMatch().get().length; + return new MatchingUtilResult(true, index); + } + + return new MatchingUtilResult(); + } + + public static MatchingUtilResult getTermIndex(String text, Pattern pattern) { + String[] parts = text.trim().toLowerCase().split(" "); + String lastPart = parts[parts.length - 1]; + Optional match = Arrays.stream(RegExpUtility.getMatches(pattern, lastPart)).findFirst(); + + if (match.isPresent()) { + int index = text.length() - text.toLowerCase().lastIndexOf(match.get().value); + return new MatchingUtilResult(true, index); + } + + return new MatchingUtilResult(); + } + + public static Boolean containsAgoLaterIndex(String text, Pattern regex) { + MatchingUtilResult result = getAgoLaterIndex(text, regex); + return result.result; + } + + public static Boolean containsTermIndex(String text, Pattern regex) { + MatchingUtilResult result = getTermIndex(text, regex); + return result.result; + } + + // Temporary solution for remove superfluous words only under the Preview mode + public static ProcessedSuperfluousWords preProcessTextRemoveSuperfluousWords(String text, StringMatcher matcher) { + List> superfluousWordMatches = removeSubMatches(matcher.find(text)); + int bias = 0; + + for (MatchResult match : superfluousWordMatches) { + text = text.substring(0, match.getStart() - bias) + text.substring(match.getEnd() - bias); + bias += match.getLength(); + } + + return new ProcessedSuperfluousWords(text, superfluousWordMatches); + } + + // Temporary solution for recover superfluous words only under the Preview mode + public static List posProcessExtractionRecoverSuperfluousWords(List extractResults, + Iterable> superfluousWordMatches, String originText) { + for (MatchResult match : superfluousWordMatches) { + int index = 0; + for (ExtractResult extractResult : extractResults.toArray(new ExtractResult[0])) { + int extractResultEnd = extractResult.getStart() + extractResult.getLength(); + if (match.getStart() > extractResult.getStart() && extractResultEnd >= match.getStart()) { + extractResult.setLength(extractResult.getLength() + match.getLength()); + extractResults.set(index, extractResult); + } + + if (match.getStart() <= extractResult.getStart()) { + extractResult.setStart(extractResult.getStart() + match.getLength()); + extractResults.set(index, extractResult); + } + index++; + } + } + + int index = 0; + for (ExtractResult er : extractResults.toArray(new ExtractResult[0])) { + er.setText(originText.substring(er.getStart(), er.getStart() + er.getLength())); + extractResults.set(index, er); + index++; + } + + return extractResults; + } + + public static List> removeSubMatches(Iterable> matchResults) { + + return StreamSupport.stream(matchResults.spliterator(), false) + .filter(item -> !StreamSupport.stream(matchResults.spliterator(), false) + .anyMatch(ritem -> (ritem.getStart() < item.getStart() && ritem.getEnd() >= item.getEnd()) || + (ritem.getStart() <= item.getStart() && ritem.getEnd() > item.getEnd()))) + .collect(Collectors.toCollection(ArrayList::new)); + } +} \ No newline at end of file diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchingUtilResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchingUtilResult.java new file mode 100644 index 000000000..b5d06180c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/MatchingUtilResult.java @@ -0,0 +1,15 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +public class MatchingUtilResult { + public final boolean result; + public final int index; + + public MatchingUtilResult(boolean result, int index) { + this.result = result; + this.index = index; + } + + public MatchingUtilResult() { + this(false, -1); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/NthBusinessDayResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/NthBusinessDayResult.java new file mode 100644 index 000000000..155bc9b6d --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/NthBusinessDayResult.java @@ -0,0 +1,14 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import java.time.LocalDateTime; +import java.util.List; + +public class NthBusinessDayResult { + public final LocalDateTime result; + public final List dateList; + + public NthBusinessDayResult(LocalDateTime result, List dateList) { + this.result = result; + this.dateList = dateList; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/RangeTimexComponents.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/RangeTimexComponents.java new file mode 100644 index 000000000..35fc10d22 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/RangeTimexComponents.java @@ -0,0 +1,11 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +public class RangeTimexComponents { + public String beginTimex; + + public String endTimex; + + public String durationTimex; + + public Boolean isValid = false; +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/RegexExtension.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/RegexExtension.java new file mode 100644 index 000000000..b4d4b8dc1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/RegexExtension.java @@ -0,0 +1,58 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.utilities.Match; +import com.microsoft.recognizers.text.utilities.RegExpUtility; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Pattern; + +public abstract class RegexExtension { + // Regex match with match length equals to text length + public static boolean isExactMatch(Pattern regex, String text, boolean trim) { + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, text)).findFirst(); + int length = trim ? text.trim().length() : text.length(); + + return (match.isPresent() && match.get().length == length); + } + + // We can't trim before match as we may use the match index later + public static ConditionalMatch matchExact(Pattern regex, String text, boolean trim) { + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, text)).findFirst(); + int length = trim ? text.trim().length() : text.length(); + + return new ConditionalMatch(match, (match.isPresent() && match.get().length == length)); + } + + // We can't trim before match as we may use the match index later + public static ConditionalMatch matchEnd(Pattern regex, String text, boolean trim) { + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, text)).reduce((f, s) -> s); + String strAfter = ""; + if (match.isPresent()) { + strAfter = text.substring(match.get().index + match.get().length); + + if (trim) { + strAfter = strAfter.trim(); + } + } + + return new ConditionalMatch(match, (match.isPresent() && StringUtility.isNullOrEmpty(strAfter))); + } + + // We can't trim before match as we may use the match index later + public static ConditionalMatch matchBegin(Pattern regex, String text, boolean trim) { + Optional match = Arrays.stream(RegExpUtility.getMatches(regex, text)).findFirst(); + String strBefore = ""; + + if (match.isPresent()) { + strBefore = text.substring(0, match.get().index); + + if (trim) { + strBefore = strBefore.trim(); + } + } + + return new ConditionalMatch(match, (match.isPresent() && StringUtility.isNullOrEmpty(strBefore))); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/StringExtension.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/StringExtension.java new file mode 100644 index 000000000..01607aea1 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/StringExtension.java @@ -0,0 +1,13 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.google.common.collect.ImmutableMap; + +public abstract class StringExtension { + public static String normalize(String text, ImmutableMap dic) { + for (ImmutableMap.Entry keyPair : dic.entrySet()) { + text = text.replace(keyPair.getKey(), keyPair.getValue()); + } + + return text; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeOfDayResolutionResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeOfDayResolutionResult.java new file mode 100644 index 000000000..1447e81a3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeOfDayResolutionResult.java @@ -0,0 +1,50 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +public class TimeOfDayResolutionResult { + private String timex; + private int beginHour; + private int endHour; + private int endMin; + + public TimeOfDayResolutionResult(String timex, int beginHour, int endHour, int endMin) { + this.timex = timex; + this.beginHour = beginHour; + this.endHour = endHour; + this.endMin = endMin; + } + + public TimeOfDayResolutionResult() { + } + + public String getTimex() { + return timex; + } + + public void setTimex(String timex) { + this.timex = timex; + } + + public int getBeginHour() { + return beginHour; + } + + public void setBeginHour(int beginHour) { + this.beginHour = beginHour; + } + + public int getEndHour() { + return endHour; + } + + public void setEndHour(int endHour) { + this.endHour = endHour; + } + + public int getEndMin() { + return endMin; + } + + public void setEndMin(int endMin) { + this.endMin = endMin; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeZoneResolutionResult.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeZoneResolutionResult.java new file mode 100644 index 000000000..7305ec7ff --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeZoneResolutionResult.java @@ -0,0 +1,26 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +public class TimeZoneResolutionResult { + + private final String value; + private final Integer utcOffsetMins; + private final String timeZoneText; + + public TimeZoneResolutionResult(String value, Integer utcOffsetMins, String timeZoneText) { + this.value = value; + this.utcOffsetMins = utcOffsetMins; + this.timeZoneText = timeZoneText; + } + + public String getValue() { + return this.value; + } + + public Integer getUtcOffsetMins() { + return this.utcOffsetMins; + } + + public String getTimeZoneText() { + return this.timeZoneText; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeZoneUtility.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeZoneUtility.java new file mode 100644 index 000000000..7720f1dca --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimeZoneUtility.java @@ -0,0 +1,64 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DateTimeOptions; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class TimeZoneUtility { + public static List mergeTimeZones(List originalErs, List timeZoneErs, String text) { + + int index = 0; + for (ExtractResult er : originalErs.toArray(new ExtractResult[0])) { + for (ExtractResult timeZoneEr : timeZoneErs) { + int begin = er.getStart() + er.getLength(); + int end = timeZoneEr.getStart(); + + if (begin < end) { + String gapText = text.substring(begin, end); + + if (StringUtility.isNullOrWhiteSpace(gapText)) { + int length = timeZoneEr.getStart() + timeZoneEr.getLength() - er.getStart(); + Map data = new HashMap<>(); + data.put(Constants.SYS_DATETIME_TIMEZONE, timeZoneEr); + + originalErs.set(index, new ExtractResult(er.getStart(), length, text.substring(er.getStart(), er.getStart() + length), er.getType(), data)); + } + } + + // Make sure timezone info propagates to longer span entity. + if (er.isOverlap(timeZoneEr)) { + Map data = new HashMap<>(); + data.put(Constants.SYS_DATETIME_TIMEZONE, timeZoneEr); + er.setData(data); + } + } + index++; + } + + return originalErs; + } + + public static boolean shouldResolveTimeZone(ExtractResult er, DateTimeOptions options) { + boolean enablePreview = options.match(DateTimeOptions.EnablePreview); + if (!enablePreview) { + return enablePreview; + } + + boolean hasTimeZoneData = false; + + if (er.getData() instanceof Map) { + Map metadata = (HashMap)er.getData(); + + if (metadata.containsKey(Constants.SYS_DATETIME_TIMEZONE)) { + hasTimeZoneData = true; + } + } + + return hasTimeZoneData; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimexUtility.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimexUtility.java new file mode 100644 index 000000000..ce68c0bb3 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/TimexUtility.java @@ -0,0 +1,298 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.recognizers.datatypes.timex.expression.TimexHelpers; +import com.microsoft.recognizers.text.datetime.Constants; +import com.microsoft.recognizers.text.datetime.DatePeriodTimexType; +import com.microsoft.recognizers.text.datetime.DateTimeResolutionKey; +import com.microsoft.recognizers.text.utilities.StringUtility; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class TimexUtility { + private static final HashMap DatePeriodTimexTypeToTimexSuffix = new HashMap() { + { + put(DatePeriodTimexType.ByDay, Constants.TimexDay); + put(DatePeriodTimexType.ByWeek, Constants.TimexWeek); + put(DatePeriodTimexType.ByMonth, Constants.TimexMonth); + put(DatePeriodTimexType.ByYear, Constants.TimexYear); + } + }; + + public static String generateCompoundDurationTimex(Map unitToTimexComponents, ImmutableMap unitValueMap) { + List unitList = new ArrayList<>(unitToTimexComponents.keySet()); + unitList.sort((x, y) -> unitValueMap.get(x) < unitValueMap.get(y) ? 1 : -1); + unitList = unitList.stream().map(t -> unitToTimexComponents.get(t)).collect(Collectors.toList()); + return TimexHelpers.generateCompoundDurationTimex(unitList); + } + + private static Boolean isTimeDurationTimex(String timex) { + return timex.startsWith(Constants.GeneralPeriodPrefix + Constants.TimeTimexPrefix); + } + + public static String getDatePeriodTimexUnitCount(LocalDateTime begin, LocalDateTime end, + DatePeriodTimexType timexType, Boolean equalDurationLength) { + String unitCount = "XX"; + if (equalDurationLength) { + switch (timexType) { + case ByDay: + unitCount = StringUtility.format((double)ChronoUnit.HOURS.between(begin, end) / 24); + break; + case ByWeek: + unitCount = Long.toString(ChronoUnit.WEEKS.between(begin, end)); + break; + case ByMonth: + unitCount = Long.toString(ChronoUnit.MONTHS.between(begin, end)); + break; + default: + unitCount = new BigDecimal((end.getYear() - begin.getYear()) + (end.getMonthValue() - begin.getMonthValue()) / 12.0).stripTrailingZeros().toString(); + } + } + + return unitCount; + } + + public static String generateDatePeriodTimex(LocalDateTime begin, LocalDateTime end, DatePeriodTimexType timexType) { + + return generateDatePeriodTimex(begin, end, timexType, null, null); + } + + public static String generateDatePeriodTimex(LocalDateTime begin, LocalDateTime end, DatePeriodTimexType timexType, + LocalDateTime alternativeBegin, LocalDateTime alternativeEnd) { + Boolean equalDurationLength; + if (alternativeBegin == null || alternativeEnd == null) { + equalDurationLength = true; + } else { + equalDurationLength = Duration.between(begin, end).equals(Duration.between(alternativeBegin, alternativeEnd)); + } + + String unitCount = getDatePeriodTimexUnitCount(begin, end, timexType, equalDurationLength); + String datePeriodTimex = "P" + unitCount + DatePeriodTimexTypeToTimexSuffix.get(timexType); + return "(" + DateTimeFormatUtil.luisDate(begin, alternativeBegin) + "," + DateTimeFormatUtil.luisDate(end, alternativeEnd) + "," + datePeriodTimex + ")"; + } + + public static String generateDatePeriodTimexStr(LocalDateTime begin, LocalDateTime end, DatePeriodTimexType timexType, + String timex1, String timex2) { + boolean boundaryValid = !DateUtil.isDefaultValue(begin) && !DateUtil.isDefaultValue(end); + String unitCount = boundaryValid ? getDatePeriodTimexUnitCount(begin, end, timexType, true) : "X"; + String datePeriodTimex = "P" + unitCount + DatePeriodTimexTypeToTimexSuffix.get(timexType); + return String.format("(%s,%s,%s)", timex1, timex2, datePeriodTimex); + } + + public static String generateWeekTimex() { + return generateWeekTimex(null); + } + + public static String generateWeekTimex(LocalDateTime monday) { + + if (monday == null) { + return Constants.TimexFuzzyYear + Constants.DateTimexConnector + Constants.TimexFuzzyWeek; + } else { + return DateTimeFormatUtil.toIsoWeekTimex(monday); + } + } + + public static String generateWeekTimex(int weekNum) { + return "W" + String.format("%02d", weekNum); + } + + public static String generateWeekendTimex() { + return generateWeekendTimex(null); + } + + public static String generateWeekendTimex(LocalDateTime date) { + if (date == null) { + return Constants.TimexFuzzyYear + Constants.DateTimexConnector + Constants.TimexFuzzyWeek + Constants.DateTimexConnector + Constants.TimexWeekend; + } else { + return DateTimeFormatUtil.toIsoWeekTimex(date) + Constants.DateTimexConnector + Constants.TimexWeekend; + } + } + + public static String generateMonthTimex() { + return generateMonthTimex(null); + } + + public static String generateMonthTimex(LocalDateTime date) { + if (date == null) { + return Constants.TimexFuzzyYear + Constants.DateTimexConnector + Constants.TimexFuzzyMonth; + } else { + return String.format("%04d-%02d", date.getYear(), date.getMonthValue()); + } + } + + public static String generateYearTimex(int year) { + return DateTimeFormatUtil.luisDate(year); + } + + public static String generateYearTimex(int year, String specialYearPrefixes) { + String yearStr = DateTimeFormatUtil.luisDate(year); + return String.format("%s%s", specialYearPrefixes, yearStr); + } + + public static String generateDurationTimex(double number, String unitStr, boolean isLessThanDay) { + if (!Constants.TimexBusinessDay.equals(unitStr)) { + if (Constants.DECADE_UNIT.equals(unitStr)) { + number = number * 10; + unitStr = Constants.TimexYear; + } else if (Constants.FORTNIGHT_UNIT.equals(unitStr)) { + number = number * 2; + unitStr = Constants.TimexWeek; + } else { + unitStr = unitStr.substring(0, 1); + } + } + + return String.format("%s%s%s%s", + Constants.GeneralPeriodPrefix, + isLessThanDay ? Constants.TimeTimexPrefix : "", + StringUtility.format(number), + unitStr); + } + + public static DatePeriodTimexType getDatePeriodTimexType(String durationTimex) { + DatePeriodTimexType result; + + String minimumUnit = durationTimex.substring(durationTimex.length() - 1); + + switch (minimumUnit) { + case Constants.TimexYear: + result = DatePeriodTimexType.ByYear; + break; + case Constants.TimexMonth: + result = DatePeriodTimexType.ByMonth; + break; + case Constants.TimexWeek: + result = DatePeriodTimexType.ByWeek; + break; + default: + result = DatePeriodTimexType.ByDay; + break; + } + + return result; + } + + public static LocalDateTime offsetDateObject(LocalDateTime date, int offset, DatePeriodTimexType timexType) { + LocalDateTime result; + + switch (timexType) { + case ByYear: + result = date.plusYears(offset); + break; + case ByMonth: + result = date.plusMonths(offset); + break; + case ByWeek: + result = date.plusDays(7 * offset); + break; + case ByDay: + result = date.plusDays(offset); + break; + default: + result = date; + break; + } + + return result; + } + + public static TimeOfDayResolutionResult parseTimeOfDay(String tod) { + switch (tod) { + case Constants.EarlyMorning: + return new TimeOfDayResolutionResult(Constants.EarlyMorning, 4, 8, 0); + case Constants.Morning: + return new TimeOfDayResolutionResult(Constants.Morning, 8, 12, 0); + case Constants.Afternoon: + return new TimeOfDayResolutionResult(Constants.Afternoon, 12, 16, 0); + case Constants.Evening: + return new TimeOfDayResolutionResult(Constants.Evening, 16, 20, 0); + case Constants.Daytime: + return new TimeOfDayResolutionResult(Constants.Daytime, 8, 18, 0); + case Constants.BusinessHour: + return new TimeOfDayResolutionResult(Constants.BusinessHour, 8, 18, 0); + case Constants.Night: + return new TimeOfDayResolutionResult(Constants.Night, 20, 23, 59); + default: + return new TimeOfDayResolutionResult(); + } + } + + public static String combineDateAndTimeTimex(String dateTimex, String timeTimex) { + return dateTimex + timeTimex; + } + + public static String generateWeekOfYearTimex(int year, int weekNum) { + String weekTimex = generateWeekTimex(weekNum); + String yearTimex = DateTimeFormatUtil.luisDate(year); + + return yearTimex + "-" + weekTimex; + } + + public static String generateWeekOfMonthTimex(int year, int month, int weekNum) { + String weekTimex = generateWeekTimex(weekNum); + String monthTimex = DateTimeFormatUtil.luisDate(year, month); + + return monthTimex + "-" + weekTimex; + } + + public static String generateDateTimePeriodTimex(String beginTimex, String endTimex, String durationTimex) { + return "(" + beginTimex + "," + endTimex + "," + durationTimex + ")"; + } + + public static RangeTimexComponents getRangeTimexComponents(String rangeTimex) { + rangeTimex = rangeTimex.replace("(", "").replace(")", ""); + String[] components = rangeTimex.split(","); + RangeTimexComponents result = new RangeTimexComponents(); + + if (components.length == 3) { + result.beginTimex = components[0]; + result.endTimex = components[1]; + result.durationTimex = components[2]; + result.isValid = true; + } + + return result; + } + + public static boolean isRangeTimex(String timex) { + return !StringUtility.isNullOrEmpty(timex) && timex.startsWith("("); + } + + public static String setTimexWithContext(String timex, DateContext context) { + return timex.replace(Constants.TimexFuzzyYear, String.format("%04d", context.getYear())); + } + + public static boolean hasDoubleTimex(String comment) { + return comment.equals(Constants.Comment_DoubleTimex); + } + + public static String mergeTimexAlternatives(String timex1, String timex2) { + if (timex1.equals(timex2)) { + return timex1; + } + return timex1 + Constants.CompositeTimexDelimiter + timex2; + } + + public static LinkedHashMap processDoubleTimex(LinkedHashMap resolutionDic, String futureKey, String pastKey, String originTimex) { + String[] timexes = originTimex.split(Constants.CompositeTimexSplit); + + if (!resolutionDic.containsKey(futureKey) || !resolutionDic.containsKey(pastKey) || timexes.length != 2) { + return resolutionDic; + } + + HashMap futureResolution = (HashMap)resolutionDic.get(futureKey); + HashMap pastResolution = (HashMap)resolutionDic.get(pastKey); + futureResolution.put(DateTimeResolutionKey.Timex, timexes[0]); + pastResolution.put(DateTimeResolutionKey.Timex, timexes[1]); + return resolutionDic; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/Token.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/Token.java new file mode 100644 index 000000000..e5a14f78e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/datetime/utilities/Token.java @@ -0,0 +1,87 @@ +package com.microsoft.recognizers.text.datetime.utilities; + +import com.microsoft.recognizers.text.ExtractResult; +import com.microsoft.recognizers.text.Metadata; + +import java.util.ArrayList; +import java.util.List; + +public class Token { + private final int start; + private final int end; + private final Metadata metadata; + + public Token(int start, int end, Metadata metadata) { + this.start = start; + this.end = end; + this.metadata = metadata; + } + + public Token(int start, int end) { + this.start = start; + this.end = end; + this.metadata = null; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public int getLength() { + return end < start ? 0 : end - start; + } + + public static List mergeAllTokens(List tokens, String text, String extractorName) { + List result = new ArrayList<>(); + List mergedTokens = new ArrayList<>(); + + tokens.sort((o1, o2) -> { + if (o1.start != o2.start) { + return o1.start - o2.start; + } + + return o2.getLength() - o1.getLength(); + }); + + for (Token token : tokens) { + if (token != null) { + boolean bAdd = true; + for (int i = 0; i < mergedTokens.size() && bAdd; i++) { + // It is included in one of the current tokens + if (token.start >= mergedTokens.get(i).start && token.end <= mergedTokens.get(i).end) { + bAdd = false; + } + + // If it contains overlaps + if (token.start > mergedTokens.get(i).start && token.start < mergedTokens.get(i).end) { + bAdd = false; + } + + // It includes one of the tokens and should replace the included one + if (token.start <= mergedTokens.get(i).start && token.end >= mergedTokens.get(i).end) { + bAdd = false; + mergedTokens.set(i, token); + } + } + + if (bAdd) { + mergedTokens.add(token); + } + } + } + + for (Token token : mergedTokens) { + String substring = text.substring(token.start, token.end); + + ExtractResult er = new ExtractResult(token.start, token.getLength(), substring, extractorName, null, token.metadata); + + result.add(er); + } + + return result; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Constants.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Constants.java new file mode 100644 index 000000000..b1e1d9ee4 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Constants.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +public class Constants { + + // Timex + public static final String TIMEX_YEAR = "Y"; + public static final String TIMEX_MONTH = "M"; + public static final String TIMEX_MONTH_FULL = "MON"; + public static final String TIMEX_WEEK = "W"; + public static final String TIMEX_DAY = "D"; + public static final String TIMEX_BUSINESS_DAY = "BD"; + public static final String TIMEX_WEEKEND = "WE"; + public static final String TIMEX_HOUR = "H"; + public static final String TIMEX_MINUTE = "M"; + public static final String TIMEX_SECOND = "S"; + public static final String TIMEX_NIGHT = "NI"; + public static final Character TIMEX_FUZZY = 'X'; + public static final String TIMEX_FUZZY_YEAR = "XXXX"; + public static final String TIMEX_FUZZY_MONTH = "XX"; + public static final String TIMEX_FUZZY_WEEK = "WXX"; + public static final String TIMEX_FUZZY_DAY = "XX"; + public static final String DATE_TIMEX_CONNECTOR = "-"; + public static final String TIME_TIMEX_CONNECTOR = ":"; + public static final String GENERAL_PERIOD_PREFIX = "P"; + public static final String TIME_TIMEX_PREFIX = "T"; + + public static final String YEAR_UNIT = "year"; + public static final String MONTH_UNIT = "month"; + public static final String WEEK_UNIT = "week"; + public static final String DAY_UNIT = "day"; + public static final String HOUR_UNIT = "hour"; + public static final String MINUTE_UNIT = "minute"; + public static final String SECOND_UNIT = "second"; + public static final String TIME_DURATION_UNIT = "s"; + + public static final String AM = "AM"; + public static final String PM = "PM"; + + public static final int INVALID_VALUE = -1; + + public static class TimexTypes { + public static final String PRESENT = "present"; + public static final String DEFINITE = "definite"; + public static final String DATE = "date"; + public static final String DATE_TIME = "datetime"; + public static final String DATE_RANGE = "daterange"; + public static final String DURATION = "duration"; + public static final String TIME = "time"; + public static final String TIME_RANGE = "timerange"; + public static final String DATE_TIME_RANGE = "datetimerange"; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/DateRange.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/DateRange.java new file mode 100644 index 000000000..6a2ecb362 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/DateRange.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.time.LocalDateTime; + +public class DateRange { + private LocalDateTime start; + private LocalDateTime end; + + public LocalDateTime getStart() { + return start; + } + + public void setStart(LocalDateTime withStart) { + this.start = withStart; + } + + public LocalDateTime getEnd() { + return end; + } + + public void setEnd(LocalDateTime withEnd) { + this.end = withEnd; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Resolution.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Resolution.java new file mode 100644 index 000000000..1e7e23cc7 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Resolution.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.util.ArrayList; +import java.util.List; + +public class Resolution { + private List values; + + public List getValues() { + return this.values; + } + + public Resolution() { + this.values = new ArrayList(); + } + + public static class Entry { + private String timex; + + private String type; + + private String value; + + private String start; + + private String end; + + public String getTimex() { + return timex; + } + + public void setTimex(String withTimex) { + this.timex = withTimex; + } + + public String getType() { + return type; + } + + public void setType(String withType) { + this.type = withType; + } + + public String getValue() { + return value; + } + + public void setValue(String withValue) { + this.value = withValue; + } + + public String getStart() { + return start; + } + + public void setStart(String withStart) { + this.start = withStart; + } + + public String getEnd() { + return end; + } + + public void setEnd(String withEnd) { + this.end = withEnd; + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Time.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Time.java new file mode 100644 index 000000000..a0f178deb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/Time.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +public class Time { + private Integer hour; + + private Integer minute; + + private Integer second; + + public Time(Integer withSeconds) { + this.hour = (int)Math.floor(withSeconds / 3600000d); + this.minute = (int)Math.floor((withSeconds - (this.hour * 3600000)) / 60000d); + this.second = (withSeconds - (this.hour * 3600000) - (this.minute * 60000)) / 1000; + } + + public Time(Integer withHour, Integer withMinute, Integer withSecond) { + this.hour = withHour; + this.minute = withMinute; + this.second = withSecond; + } + + public Integer getTime() { + return (this.second * 1000) + (this.minute * 60000) + (this.hour * 3600000); + } + + public Integer getHour() { + return hour; + } + + public void setHour(Integer withHour) { + this.hour = withHour; + } + + public Integer getMinute() { + return minute; + } + + public void setMinute(Integer withMinute) { + this.minute = withMinute; + } + + public Integer getSecond() { + return second; + } + + public void setSecond(Integer withSecond) { + this.second = withSecond; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimeRange.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimeRange.java new file mode 100644 index 000000000..541bff588 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimeRange.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +public class TimeRange { + private Time start; + + private Time end; + + public Time getStart() { + return start; + } + + public void setStart(Time withStart) { + this.start = withStart; + } + + public Time getEnd() { + return end; + } + + public void setEnd(Time withEnd) { + this.end = withEnd; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexConstraintsHelper.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexConstraintsHelper.java new file mode 100644 index 000000000..76d1953a6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexConstraintsHelper.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.util.List; + +public class TimexConstraintsHelper { + public static List collapseTimeRanges(List ranges) { + List r = ranges; + + while (TimexConstraintsHelper.innerCollapseTimeRanges(r)) { + + } + + r.sort((a, b) -> a.getStart().getTime() - b.getStart().getTime()); + + return r; + } + + public static List collapseDateRanges(List ranges) { + List r = ranges; + + while (TimexConstraintsHelper.innerCollapseDateRanges(r)) { + + } + + r.sort((a, b) -> a.getStart().compareTo(b.getStart())); + return r; + } + + public static Boolean isOverlapping(TimeRange r1, TimeRange r2) { + return (r1.getEnd().getTime() > r2.getStart().getTime() && r1.getStart().getTime() <= r2.getStart().getTime()) || + (r1.getStart().getTime() < r2.getEnd().getTime() && + r1.getStart().getTime() >= r2.getStart().getTime()); + } + + private static Boolean isOverlapping(DateRange r1, DateRange r2) { + return (r1.getEnd().isAfter(r2.getStart()) && (r1.getStart().isBefore(r2.getStart()) || r1.getStart().isEqual(r2.getStart()))) || + (r1.getStart().isBefore(r2.getEnd()) && (r1.getStart().isAfter(r2.getStart()) || r1.getStart().isEqual(r2.getStart()))); + } + + private static TimeRange collapseOverlapping(TimeRange r1, TimeRange r2) { + return new TimeRange() { + { + setStart(new Time(Math.max(r1.getStart().getTime(), r2.getStart().getTime()))); + setEnd(new Time(Math.min(r1.getEnd().getTime(), r2.getEnd().getTime()))); + } + }; + } + + private static DateRange collapseOverlapping(DateRange r1, DateRange r2) { + return new DateRange() { + { + setStart(r1.getStart().compareTo(r2.getStart()) > 0 ? r1.getStart() : r2.getStart()); + setEnd(r1.getEnd().compareTo(r2.getEnd()) < 0 ? r1.getEnd() : r2.getEnd()); + } + }; + } + + private static Boolean innerCollapseTimeRanges(List ranges) { + if (ranges.size() == 1) { + return false; + } + + for (int i = 0; i < ranges.size(); i++) { + TimeRange r1 = ranges.get(i); + for (int j = i + 1; j < ranges.size(); j++) { + TimeRange r2 = ranges.get(j); + if (TimexConstraintsHelper.isOverlapping(r1, r2)) { + ranges.subList(i, 1).clear(); + ranges.subList(j - 1, 1).clear(); + ranges.add(TimexConstraintsHelper.collapseOverlapping(r1, r2)); + return true; + } + } + } + + return false; + } + + private static Boolean innerCollapseDateRanges(List ranges) { + if (ranges.size() == 1) { + return false; + } + + for (int i = 0; i < ranges.size(); i++) { + DateRange r1 = ranges.get(i); + for (int j = i + 1; j < ranges.size(); j++) { + DateRange r2 = ranges.get(j); + if (TimexConstraintsHelper.isOverlapping(r1, r2)) { + ranges.subList(i, 1).clear(); + ranges.subList(j - 1, 1).clear(); + ranges.add(TimexConstraintsHelper.collapseOverlapping(r1, r2)); + return true; + } + } + } + + return false; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexConvert.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexConvert.java new file mode 100644 index 000000000..9b7844177 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexConvert.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import com.microsoft.recognizers.datatypes.timex.expression.english.TimexConvertEnglish; + +public class TimexConvert { + public static String convertTimexToString(TimexProperty timex) { + return TimexConvertEnglish.convertTimexToString(timex); + } + + public static String convertTimexSetToString(TimexSet timexSet) { + return TimexConvertEnglish.convertTimexSetToString(timexSet); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexCreator.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexCreator.java new file mode 100644 index 000000000..94920fdfd --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexCreator.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.math.BigDecimal; +import java.time.DayOfWeek; +import java.time.LocalDateTime; + +public class TimexCreator { + // The following constants are consistent with the Recognizer results + public static final String MONDAY = "XXXX-WXX-1"; + public static final String TUESDAY = "XXXX-WXX-2"; + public static final String WEDNESDAY = "XXXX-WXX-3"; + public static final String THURSDAY = "XXXX-WXX-4"; + public static final String FRIDAY = "XXXX-WXX-5"; + public static final String SATURDAY = "XXXX-WXX-6"; + public static final String SUNDAY = "XXXX-WXX-7"; + public static final String MORNING = "(T08,T12,PT4H)"; + public static final String AFTERNOON = "(T12,T16,PT4H)"; + public static final String EVENING = "(T16,T20,PT4H)"; + public static final String DAYTIME = "(T08,T18,PT10H)"; + public static final String NIGHT = "(T20,T24,PT10H)"; + + public static String today(LocalDateTime date) { + return TimexProperty.fromDate(date == null ? LocalDateTime.now() : date).getTimexValue(); + } + + public static String tomorrow(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + d = d.plusDays(1); + return TimexProperty.fromDate(d).getTimexValue(); + } + + public static String yesterday(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + d = d.plusDays(-1); + return TimexProperty.fromDate(d).getTimexValue(); + } + + public static String weekFromToday(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + TimexProperty t = TimexProperty.fromDate(d); + t.setDays(new BigDecimal(7)); + return t.getTimexValue(); + } + + public static String weekBackFromToday(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + d = d.plusDays(-7); + TimexProperty t = TimexProperty.fromDate(d); + t.setDays(new BigDecimal(7)); + return t.getTimexValue(); + } + + public static String thisWeek(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + d = d.plusDays(-7); + LocalDateTime start = TimexDateHelpers.dateOfNextDay(DayOfWeek.MONDAY, d); + TimexProperty t = TimexProperty.fromDate(start); + t.setDays(new BigDecimal(7)); + return t.getTimexValue(); + } + + public static String nextWeek(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + LocalDateTime start = TimexDateHelpers.dateOfNextDay(DayOfWeek.MONDAY, d); + TimexProperty t = TimexProperty.fromDate(start); + t.setDays(new BigDecimal(7)); + return t.getTimexValue(); + } + + public static String lastWeek(LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + LocalDateTime start = TimexDateHelpers.dateOfLastDay(DayOfWeek.MONDAY, d); + start = start.plusDays(-7); + TimexProperty t = TimexProperty.fromDate(start); + t.setDays(new BigDecimal(7)); + return t.getTimexValue(); + } + + public static String nextWeeksFromToday(Integer n, LocalDateTime date) { + LocalDateTime d = (date == null) ? LocalDateTime.now() : date; + TimexProperty t = TimexProperty.fromDate(d); + t.setDays(new BigDecimal(n * 7)); + return t.getTimexValue(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexDateHelpers.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexDateHelpers.java new file mode 100644 index 000000000..cdd5707fb --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexDateHelpers.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public class TimexDateHelpers { + public static LocalDateTime tomorrow(LocalDateTime date) { + date = date.plusDays(1); + return date; + } + + public static LocalDateTime yesterday(LocalDateTime date) { + date = date.plusDays(-1); + return date; + } + + public static Boolean datePartEquals(LocalDateTime dateX, LocalDateTime dateY) { + return (dateX.getYear() == dateY.getYear()) && + (dateX.getMonthValue() == dateY.getMonthValue()) && + (dateX.getDayOfMonth() == dateY.getDayOfMonth()); + } + + public static boolean isDateInWeek(LocalDateTime date, LocalDateTime startOfWeek) { + LocalDateTime d = startOfWeek; + for (int i = 0; i < 7; i++) { + if (TimexDateHelpers.datePartEquals(date, d)) { + return true; + } + + d = d.plusDays(1); + } + + return false; + } + + public static Boolean isThisWeek(LocalDateTime date, LocalDateTime referenceDate) { + // Note ISO 8601 week starts on a Monday + LocalDateTime startOfWeek = referenceDate; + while (TimexDateHelpers.getUSDayOfWeek(startOfWeek.getDayOfWeek()) > TimexDateHelpers.getUSDayOfWeek(DayOfWeek.MONDAY)) { + startOfWeek = startOfWeek.plusDays(-1); + } + + return TimexDateHelpers.isDateInWeek(date, startOfWeek); + } + + public static Boolean isNextWeek(LocalDateTime date, LocalDateTime referenceDate) { + LocalDateTime nextWeekDate = referenceDate; + nextWeekDate = nextWeekDate.plusDays(7); + return TimexDateHelpers.isThisWeek(date, nextWeekDate); + } + + public static Boolean isLastWeek(LocalDateTime date, LocalDateTime referenceDate) { + LocalDateTime nextWeekDate = referenceDate; + nextWeekDate = nextWeekDate.plusDays(-7); + return TimexDateHelpers.isThisWeek(date, nextWeekDate); + } + + public static Integer weekOfYear(LocalDateTime date) { + LocalDateTime ds = LocalDateTime.of(date.getYear(), 1, 1, 0, 0); + LocalDateTime de = LocalDateTime.of(date.getYear(), date.getMonthValue(), date.getDayOfMonth(), 0, 0); + Integer weeks = 1; + + while (ds.compareTo(de) < 0) { + Integer dayOfWeek = TimexDateHelpers.getUSDayOfWeek(ds.getDayOfWeek()); + + Integer isoDayOfWeek = (dayOfWeek == 0) ? 7 : dayOfWeek; + if (isoDayOfWeek == 7) { + weeks++; + } + + ds = ds.plusDays(1); + } + + return weeks; + } + + public static String fixedFormatNumber(Integer n, Integer size) { + return String.format("%1$" + size + "s", n.toString()).replace(' ', '0'); + } + + public static LocalDateTime dateOfLastDay(DayOfWeek day, LocalDateTime referenceDate) { + LocalDateTime result = referenceDate; + result = result.plusDays(-1); + + while (result.getDayOfWeek() != day) { + result = result.plusDays(-1); + } + + return result; + } + + public static LocalDateTime dateOfNextDay(DayOfWeek day, LocalDateTime referenceDate) { + LocalDateTime result = referenceDate; + result = result.plusDays(1); + + while (result.getDayOfWeek() != day) { + result = result.plusDays(1); + } + + return result; + } + + public static List datesMatchingDay(DayOfWeek day, LocalDateTime start, LocalDateTime end) { + List result = new ArrayList(); + LocalDateTime d = start; + + while (!TimexDateHelpers.datePartEquals(d, end)) { + if (d.getDayOfWeek() == day) { + result.add(d); + } + + d = d.plusDays(1); + } + + return result; + } + + public static Integer getUSDayOfWeek(DayOfWeek dayOfWeek) { + return dayOfWeek.getValue() % 7; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexFormat.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexFormat.java new file mode 100644 index 000000000..c7f8d2eba --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexFormat.java @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; + +public class TimexFormat { + public static String format(TimexProperty timex) { + HashSet types = timex.getTypes().size() != 0 ? timex.getTypes() : TimexInference.infer(timex); + + if (types.contains(Constants.TimexTypes.PRESENT)) { + return "PRESENT_REF"; + } + + if ((types.contains(Constants.TimexTypes.DATE_TIME_RANGE) || types.contains(Constants.TimexTypes.DATE_RANGE) || + types.contains(Constants.TimexTypes.TIME_RANGE)) && types.contains(Constants.TimexTypes.DURATION)) { + TimexRange range = TimexHelpers.expandDateTimeRange(timex); + return String.format("(%1$s,%2$s,%3$s)", TimexFormat.format(range.getStart()), + TimexFormat.format(range.getEnd()), TimexFormat.format(range.getDuration())); + } + + if (types.contains(Constants.TimexTypes.DATE_TIME_RANGE)) { + return String.format("%1$s%2$s", TimexFormat.formatDate(timex), TimexFormat.formatTimeRange(timex)); + } + + if (types.contains(Constants.TimexTypes.DATE_RANGE)) { + return TimexFormat.formatDateRange(timex); + } + + if (types.contains(Constants.TimexTypes.TIME_RANGE)) { + return TimexFormat.formatTimeRange(timex); + } + + if (types.contains(Constants.TimexTypes.DATE_TIME)) { + return String.format("%1$s%2$s", TimexFormat.formatDate(timex), TimexFormat.formatTime(timex)); + } + + if (types.contains(Constants.TimexTypes.DURATION)) { + return TimexFormat.formatDuration(timex); + } + + if (types.contains(Constants.TimexTypes.DATE)) { + return TimexFormat.formatDate(timex); + } + + if (types.contains(Constants.TimexTypes.TIME)) { + return TimexFormat.formatTime(timex); + } + + return new String(); + } + + private static String formatDuration(TimexProperty timex) { + List timexList = new ArrayList(); + NumberFormat nf = NumberFormat.getInstance(Locale.getDefault()); + + if (timex.getYears() != null) { + nf.setMaximumFractionDigits(timex.getYears().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Year, + timex.getYears() != null ? timex.getYears() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + if (timex.getMonths() != null) { + nf.setMaximumFractionDigits(timex.getMonths().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Month, + timex.getMonths() != null ? timex.getMonths() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + if (timex.getWeeks() != null) { + nf.setMaximumFractionDigits(timex.getWeeks().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Week, + timex.getWeeks() != null ? timex.getWeeks() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + if (timex.getDays() != null) { + nf.setMaximumFractionDigits(timex.getDays().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Day, + timex.getDays() != null ? timex.getDays() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + if (timex.getHours() != null) { + nf.setMaximumFractionDigits(timex.getHours().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Hour, + timex.getHours() != null ? timex.getHours() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + if (timex.getMinutes() != null) { + nf.setMaximumFractionDigits(timex.getMinutes().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Minute, + timex.getMinutes() != null ? timex.getMinutes() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + if (timex.getSeconds() != null) { + nf.setMaximumFractionDigits(timex.getSeconds().scale()); + timexList.add(TimexHelpers.generateDurationTimex(TimexUnit.Second, + timex.getSeconds() != null ? timex.getSeconds() : BigDecimal.valueOf(Constants.INVALID_VALUE))); + } + + return TimexHelpers.generateCompoundDurationTimex(timexList); + } + + private static String formatTime(TimexProperty timex) { + if (timex.getMinute() == 0 && timex.getSecond() == 0) { + return String.format("T%s", TimexDateHelpers.fixedFormatNumber(timex.getHour(), 2)); + } + + if (timex.getSecond() == 0) { + return String.format("T%1$s:%2$s", TimexDateHelpers.fixedFormatNumber(timex.getHour(), 2), + TimexDateHelpers.fixedFormatNumber(timex.getMinute(), 2)); + } + + return String.format("T%1$s:%2$s:%3$s", TimexDateHelpers.fixedFormatNumber(timex.getHour(), 2), + TimexDateHelpers.fixedFormatNumber(timex.getMinute(), 2), + TimexDateHelpers.fixedFormatNumber(timex.getSecond(), 2)); + } + + private static String formatDate(TimexProperty timex) { + Integer year = timex.getYear() != null ? timex.getYear() : Constants.INVALID_VALUE; + Integer month = timex.getWeekOfYear() != null ? timex.getWeekOfYear() + : (timex.getMonth() != null ? timex.getMonth() : Constants.INVALID_VALUE); + Integer day = timex.getDayOfWeek() != null ? timex.getDayOfWeek() + : timex.getDayOfMonth() != null ? timex.getDayOfMonth() : Constants.INVALID_VALUE; + Integer weekOfMonth = timex.getWeekOfMonth() != null ? timex.getWeekOfMonth() : Constants.INVALID_VALUE; + + return TimexHelpers.generateDateTimex(year, month, day, weekOfMonth, timex.getDayOfWeek() != null); + } + + private static String formatDateRange(TimexProperty timex) { + if (timex.getYear() != null && timex.getWeekOfYear() != null && timex.getWeekend() != null) { + return String.format("%1$s-W%2$s-WE", TimexDateHelpers.fixedFormatNumber(timex.getYear(), 4), + TimexDateHelpers.fixedFormatNumber(timex.getWeekOfYear(), 2)); + } + + if (timex.getYear() != null && timex.getWeekOfYear() != null) { + return String.format("%1$s-W%2$s", TimexDateHelpers.fixedFormatNumber(timex.getYear(), 4), + TimexDateHelpers.fixedFormatNumber(timex.getWeekOfYear(), 2)); + } + + if (timex.getYear() != null && timex.getMonth() != null && timex.getWeekOfMonth() != null) { + return String.format("%1$s-%2$s-W%3$s", TimexDateHelpers.fixedFormatNumber(timex.getYear(), 4), + TimexDateHelpers.fixedFormatNumber(timex.getMonth(), 2), + TimexDateHelpers.fixedFormatNumber(timex.getWeekOfMonth(), 2)); + } + + if (timex.getYear() != null && timex.getSeason() != null) { + return String.format("%1$s-%2$s", TimexDateHelpers.fixedFormatNumber(timex.getYear(), 4), + timex.getSeason()); + } + + if (timex.getSeason() != null) { + return timex.getSeason(); + } + + if (timex.getYear() != null && timex.getMonth() != null) { + return String.format("%1$s-%2$s", TimexDateHelpers.fixedFormatNumber(timex.getYear(), 4), + TimexDateHelpers.fixedFormatNumber(timex.getMonth(), 2)); + } + + if (timex.getYear() != null) { + return TimexDateHelpers.fixedFormatNumber(timex.getYear(), 4); + } + + if (timex.getMonth() != null && timex.getWeekOfMonth() != null && timex.getDayOfWeek() != null) { + return String.format("%1$s-%2$s-%3$s-%4$s-%5$s", Constants.TIMEX_FUZZY_YEAR, + TimexDateHelpers.fixedFormatNumber(timex.getMonth(), 2), Constants.TIMEX_FUZZY_WEEK, + timex.getWeekOfMonth(), timex.getDayOfWeek()); + } + + if (timex.getMonth() != null && timex.getWeekOfMonth() != null) { + return String.format("%1$s-%2$s-W%3$02d", Constants.TIMEX_FUZZY_YEAR, + TimexDateHelpers.fixedFormatNumber(timex.getMonth(), 2), timex.getWeekOfMonth()); + } + + if (timex.getMonth() != null) { + return String.format("%1$s-%2$s", Constants.TIMEX_FUZZY_YEAR, + TimexDateHelpers.fixedFormatNumber(timex.getMonth(), 2)); + } + + return new String(); + } + + private static String formatTimeRange(TimexProperty timex) { + if (timex.getPartOfDay() != null) { + return String.format("T%s", timex.getPartOfDay()); + } + + return new String(); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexHelpers.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexHelpers.java new file mode 100644 index 000000000..322c696de --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexHelpers.java @@ -0,0 +1,515 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.math.BigDecimal; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.temporal.TemporalField; +import java.time.temporal.WeekFields; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.lang3.tuple.Pair; + +public class TimexHelpers { + public static final HashMap TIMEX_UNIT_TO_STRING_MAP = new HashMap() { + { + put(TimexUnit.Year, Constants.TIMEX_YEAR); + put(TimexUnit.Month, Constants.TIMEX_MONTH); + put(TimexUnit.Week, Constants.TIMEX_WEEK); + put(TimexUnit.Day, Constants.TIMEX_DAY); + put(TimexUnit.Hour, Constants.TIMEX_HOUR); + put(TimexUnit.Minute, Constants.TIMEX_MINUTE); + put(TimexUnit.Second, Constants.TIMEX_SECOND); + } + }; + + public static final List TimeTimexUnitList = Arrays.asList(TimexUnit.Hour, TimexUnit.Minute, + TimexUnit.Second); + + public static TimexRange expandDateTimeRange(TimexProperty timex) { + HashSet types = timex.getTypes().size() != 0 ? timex.getTypes() : TimexInference.infer(timex); + + if (types.contains(Constants.TimexTypes.DURATION)) { + TimexProperty start = TimexHelpers.cloneDateTime(timex); + TimexProperty duration = TimexHelpers.cloneDuration(timex); + return new TimexRange() { + { + setStart(start); + setEnd(TimexHelpers.timexDateTimeAdd(start, duration)); + setDuration(duration); + } + }; + } else { + if (timex.getYear() != null) { + Pair dateRange; + if (timex.getMonth() != null && timex.getWeekOfMonth() != null) { + dateRange = TimexHelpers.monthWeekDateRange(timex.getYear(), timex.getMonth(), + timex.getWeekOfMonth()); + } else if (timex.getMonth() != null) { + dateRange = TimexHelpers.monthDateRange(timex.getYear(), timex.getMonth()); + } else if (timex.getWeekOfYear() != null) { + dateRange = TimexHelpers.yearWeekDateRange(timex.getYear(), timex.getWeekOfYear(), + timex.getWeekend()); + } else { + dateRange = TimexHelpers.yearDateRange(timex.getYear()); + } + return new TimexRange() { + { + setStart(dateRange.getLeft()); + setEnd(dateRange.getRight()); + } + }; + } + } + + return new TimexRange() { + { + setStart(new TimexProperty()); + setEnd(new TimexProperty()); + } + }; + } + + public static TimexRange expandTimeRange(TimexProperty timex) { + if (!timex.getTypes().contains(Constants.TimexTypes.TIME_RANGE)) { + throw new IllegalArgumentException("argument must be a timerange: timex"); + } + + if (timex.getPartOfDay() != null) { + switch (timex.getPartOfDay()) { + case "DT": + timex = new TimexProperty(TimexCreator.DAYTIME); + break; + case "MO": + timex = new TimexProperty(TimexCreator.MORNING); + break; + case "AF": + timex = new TimexProperty(TimexCreator.AFTERNOON); + break; + case "EV": + timex = new TimexProperty(TimexCreator.EVENING); + break; + case "NI": + timex = new TimexProperty(TimexCreator.NIGHT); + break; + default: + throw new IllegalArgumentException("unrecognized part of day timerange: timex"); + } + } + + Integer hour = timex.getHour(); + Integer minute = timex.getMinute(); + Integer second = timex.getSecond(); + TimexProperty start = new TimexProperty() { + { + setHour(hour); + setMinute(minute); + setSecond(second); + } + }; + TimexProperty duration = TimexHelpers.cloneDuration(timex); + + return new TimexRange() { + { + setStart(start); + setEnd(TimexHelpers.timeAdd(start, duration)); + setDuration(duration); + } + }; + } + + public static TimexProperty timexDateAdd(TimexProperty start, TimexProperty duration) { + if (start.getDayOfWeek() != null) { + TimexProperty end = start.clone(); + if (duration.getDays() != null) { + Integer newDayOfWeek = end.getDayOfWeek() + (int)Math.round(duration.getDays().doubleValue()); + end.setDayOfWeek(newDayOfWeek); + } + + return end; + } + + if (start.getMonth() != null && start.getDayOfMonth() != null) { + Double durationDays = null; + if (duration.getDays() != null) { + durationDays = duration.getDays().doubleValue(); + } + + if (durationDays == null && duration.getWeeks() != null) { + durationDays = 7 * duration.getWeeks().doubleValue(); + } + + if (durationDays != null) { + if (start.getYear() != null) { + LocalDateTime d = LocalDateTime.of(start.getYear(), start.getMonth(), start.getDayOfMonth(), 0, 0, + 0); + LocalDateTime d2 = d.plusDays(durationDays.longValue()); + return new TimexProperty() { + { + setYear(d2.getYear()); + setMonth(d2.getMonthValue()); + setDayOfMonth(d2.getDayOfMonth()); + } + }; + } else { + LocalDateTime d = LocalDateTime.of(2001, start.getMonth(), start.getDayOfMonth(), 0, 0, 0); + LocalDateTime d2 = d.plusDays(durationDays.longValue()); + return new TimexProperty() { + { + setMonth(d2.getMonthValue()); + setDayOfMonth(d2.getDayOfMonth()); + } + }; + } + } + + if (duration.getYears() != null) { + if (start.getYear() != null) { + return new TimexProperty() { + { + setYear(start.getYear() + (int)Math.round(duration.getYears().doubleValue())); + setMonth(start.getMonth()); + setDayOfMonth(start.getDayOfMonth()); + } + }; + } + } + + if (duration.getMonths() != null) { + if (start.getMonth() != null) { + return new TimexProperty() { + { + setYear(start.getYear()); + setMonth(start.getMonth() + (int)Math.round(duration.getMonths().doubleValue())); + setDayOfMonth(start.getDayOfMonth()); + } + }; + } + } + } + + return start; + } + + public static String generateCompoundDurationTimex(List timexList) { + Boolean isTimeDurationAlreadyExist = false; + StringBuilder timexBuilder = new StringBuilder(Constants.GENERAL_PERIOD_PREFIX); + + for (String timexComponent : timexList) { + // The Time Duration component occurs first time + if (!isTimeDurationAlreadyExist && isTimeDurationTimex(timexComponent)) { + timexBuilder.append(Constants.TIME_TIMEX_PREFIX.concat(getDurationTimexWithoutPrefix(timexComponent))); + isTimeDurationAlreadyExist = true; + } else { + timexBuilder.append(getDurationTimexWithoutPrefix(timexComponent)); + } + } + + return timexBuilder.toString(); + } + + public static String generateDateTimex(Integer year, Integer monthOrWeekOfYear, Integer day, Integer weekOfMonth, + boolean byWeek) { + String yearString = year == Constants.INVALID_VALUE ? Constants.TIMEX_FUZZY_YEAR + : TimexDateHelpers.fixedFormatNumber(year, 4); + String monthWeekString = monthOrWeekOfYear == Constants.INVALID_VALUE ? Constants.TIMEX_FUZZY_MONTH + : TimexDateHelpers.fixedFormatNumber(monthOrWeekOfYear, 2); + String dayString; + if (byWeek) { + dayString = day.toString(); + if (weekOfMonth != Constants.INVALID_VALUE) { + monthWeekString = monthWeekString + String.format("-%s-", Constants.TIMEX_FUZZY_WEEK) + + weekOfMonth.toString(); + } else { + monthWeekString = Constants.TIMEX_WEEK + monthWeekString; + } + } else { + dayString = day == Constants.INVALID_VALUE ? Constants.TIMEX_FUZZY_DAY + : TimexDateHelpers.fixedFormatNumber(day, 2); + } + + return String.join("-", yearString, monthWeekString, dayString); + } + + public static String generateDurationTimex(TimexUnit unit, BigDecimal value) { + if (value.intValue() == Constants.INVALID_VALUE) { + return new String(); + } + + StringBuilder timexBuilder = new StringBuilder(Constants.GENERAL_PERIOD_PREFIX); + if (TimeTimexUnitList.contains(unit)) { + timexBuilder.append(Constants.TIME_TIMEX_PREFIX); + } + + timexBuilder.append(value.toString()); + timexBuilder.append(TIMEX_UNIT_TO_STRING_MAP.get(unit)); + return timexBuilder.toString(); + } + + public static TimexProperty timexTimeAdd(TimexProperty start, TimexProperty duration) { + + TimexProperty result = start.clone(); + if (duration.getMinutes() != null) { + result.setMinute(result.getMinute() + (int)Math.round(duration.getMinutes().doubleValue())); + + if (result.getMinute() > 59) { + result.setHour(((result.getHour() != null) ? result.getHour() : 0) + 1); + result.setMinute(result.getMinute() % 60); + } + } + + if (duration.getHours() != null) { + result.setHour(result.getHour() + (int)Math.round(duration.getHours().doubleValue())); + } + + if (result.getHour() != null && result.getHour() > 23) { + Double days = Math.floor(result.getHour() / 24d); + Integer hour = result.getHour() % 24; + result.setHour(hour); + + if (result.getYear() != null && result.getMonth() != null && result.getDayOfMonth() != null) { + LocalDateTime d = LocalDateTime.of(result.getYear(), result.getMonth(), result.getDayOfMonth(), 0, 0, + 0); + d = d.plusDays(days.longValue()); + + result.setYear(d.getYear()); + result.setMonth(d.getMonthValue()); + result.setDayOfMonth(d.getDayOfMonth()); + + return result; + } + + if (result.getDayOfWeek() != null) { + result.setDayOfWeek(result.getDayOfWeek() + (int)Math.round(days)); + return result; + } + } + + return result; + } + + public static TimexProperty timexDateTimeAdd(TimexProperty start, TimexProperty duration) { + return TimexHelpers.timexTimeAdd(TimexHelpers.timexDateAdd(start, duration), duration); + } + + public static LocalDateTime dateFromTimex(TimexProperty timex) { + Integer year = timex.getYear() != null ? timex.getYear() : 2001; + Integer month = timex.getMonth() != null ? timex.getMonth() : 1; + Integer day = timex.getDayOfMonth() != null ? timex.getDayOfMonth() : 1; + Integer hour = timex.getHour() != null ? timex.getHour() : 0; + Integer minute = timex.getMinute() != null ? timex.getMinute() : 0; + Integer second = timex.getSecond() != null ? timex.getSecond() : 0; + LocalDateTime date = LocalDateTime.of(year, month, day, hour, minute, second); + + return date; + } + + public static Time timeFromTimex(TimexProperty timex) { + Integer hour = timex.getHour() != null ? timex.getHour() : 0; + Integer minute = timex.getMinute() != null ? timex.getMinute() : 0; + Integer second = timex.getSecond() != null ? timex.getSecond() : 0; + return new Time(hour, minute, second); + } + + public static DateRange dateRangeFromTimex(TimexProperty timex) { + TimexRange expanded = TimexHelpers.expandDateTimeRange(timex); + return new DateRange() { + { + setStart(TimexHelpers.dateFromTimex(expanded.getStart())); + setEnd(TimexHelpers.dateFromTimex(expanded.getEnd())); + } + }; + } + + public static TimeRange timeRangeFromTimex(TimexProperty timex) { + TimexRange expanded = TimexHelpers.expandTimeRange(timex); + return new TimeRange() { + { + setStart(TimexHelpers.timeFromTimex(expanded.getStart())); + setEnd(TimexHelpers.timeFromTimex(expanded.getEnd())); + } + }; + } + + public static String formatResolvedDateValue(String dateValue, String timeValue) { + return String.format("%1$s %2$s", dateValue, timeValue); + } + + public static Pair monthWeekDateRange(Integer year, Integer month, + Integer weekOfMonth) { + LocalDateTime start = TimexHelpers.generateMonthWeekDateStart(year, month, weekOfMonth); + LocalDateTime end = start.plusDays(7); + TimexProperty value1 = new TimexProperty() { + { + setYear(start.getYear()); + setMonth(start.getMonth().getValue()); + setDayOfMonth(start.getDayOfMonth()); + } + }; + TimexProperty value2 = new TimexProperty() { + { + setYear(end.getYear()); + setMonth(end.getMonth().getValue()); + setDayOfMonth(end.getDayOfMonth()); + } + }; + return Pair.of(value1, value2); + } + + public static Pair monthDateRange(Integer year, Integer month) { + TimexProperty value1 = new TimexProperty() { + { + setYear(year); + setMonth(month); + setDayOfMonth(1); + } + }; + TimexProperty value2 = new TimexProperty() { + { + setYear(month == 12 ? year + 1 : year); + setMonth(month == 12 ? 1 : month + 1); + setDayOfMonth(1); + } + }; + return Pair.of(value1, value2); + } + + public static Pair yearDateRange(Integer year) { + TimexProperty value1 = new TimexProperty() { + { + setYear(year); + setMonth(1); + setDayOfMonth(1); + } + }; + TimexProperty value2 = new TimexProperty() { + { + setYear(year + 1); + setMonth(1); + setDayOfMonth(1); + } + }; + return Pair.of(value1, value2); + } + + public static Pair yearWeekDateRange(Integer year, Integer weekOfYear, + Boolean isWeekend) { + LocalDateTime firstMondayInWeek = TimexHelpers.firstDateOfWeek(year, weekOfYear, null); + + LocalDateTime start = (isWeekend == null || !isWeekend) ? firstMondayInWeek + : TimexDateHelpers.dateOfNextDay(DayOfWeek.SATURDAY, firstMondayInWeek); + LocalDateTime end = firstMondayInWeek.plusDays(7); + TimexProperty value1 = new TimexProperty() { + { + setYear(start.getYear()); + setMonth(start.getMonth().getValue()); + setDayOfMonth(start.getDayOfMonth()); + } + }; + TimexProperty value2 = new TimexProperty() { + { + setYear(end.getYear()); + setMonth(end.getMonth().getValue()); + setDayOfMonth(end.getDayOfMonth()); + } + }; + return Pair.of(value1, value2); + } + + // this is based on + // https://stackoverflow.com/questions/19901666/get-date-of-first-and-last-day-of-week-knowing-week-number/34727270 + public static LocalDateTime firstDateOfWeek(Integer year, Integer weekOfYear, Locale cultureInfo) { + // ISO uses FirstFourDayWeek, and Monday as first day of week, according to + // https://en.wikipedia.org/wiki/ISO_8601 + LocalDateTime jan1 = LocalDateTime.of(year, 1, 1, 0, 0); + Integer daysOffset = DayOfWeek.MONDAY.getValue() - TimexDateHelpers.getUSDayOfWeek(jan1.getDayOfWeek()); + LocalDateTime firstWeekDay = jan1; + firstWeekDay = firstWeekDay.plusDays(daysOffset); + + TemporalField woy = WeekFields.ISO.weekOfYear(); + Integer firstWeek = jan1.get(woy); + + if ((firstWeek <= 1 || firstWeek >= 52) && daysOffset >= -3) { + weekOfYear -= 1; + } + + firstWeekDay = firstWeekDay.plusDays(weekOfYear * 7); + + return firstWeekDay; + } + + public static LocalDateTime generateMonthWeekDateStart(Integer year, Integer month, Integer weekOfMonth) { + LocalDateTime dateInWeek = LocalDateTime.of(year, month, 1 + ((weekOfMonth - 1) * 7), 0, 0); + + // Align the date of the week according to Thursday, base on ISO 8601, + // https://en.wikipedia.org/wiki/ISO_8601 + if (dateInWeek.getDayOfWeek().getValue() > DayOfWeek.THURSDAY.getValue()) { + dateInWeek = dateInWeek.plusDays(7 - dateInWeek.getDayOfWeek().getValue() + 1); + } else { + dateInWeek = dateInWeek.plusDays(1 - dateInWeek.getDayOfWeek().getValue()); + } + + return dateInWeek; + } + + private static TimexProperty timeAdd(TimexProperty start, TimexProperty duration) { + Integer second = start.getSecond() + + (int)(duration.getSeconds() != null ? duration.getSeconds().intValue() : 0); + Integer minute = start.getMinute() + second / 60 + + (duration.getMinutes() != null ? duration.getMinutes().intValue() : 0); + Integer hour = start.getHour() + (minute / 60) + + (duration.getHours() != null ? duration.getHours().intValue() : 0); + + return new TimexProperty() { + { + setHour((hour == 24 && minute % 60 == 0 && second % 60 == 0) ? hour : hour % 24); + setMinute(minute % 60); + setSecond(second % 60); + } + }; + } + + private static TimexProperty cloneDateTime(TimexProperty timex) { + TimexProperty result = timex.clone(); + result.setYears(null); + result.setMonths(null); + result.setWeeks(null); + result.setDays(null); + result.setHours(null); + result.setMinutes(null); + result.setSeconds(null); + return result; + } + + private static TimexProperty cloneDuration(TimexProperty timex) { + TimexProperty result = timex.clone(); + result.setYear(null); + result.setMonth(null); + result.setDayOfMonth(null); + result.setDayOfWeek(null); + result.setWeekOfYear(null); + result.setWeekOfMonth(null); + result.setSeason(null); + result.setHour(null); + result.setMinute(null); + result.setSecond(null); + result.setWeekend(null); + result.setPartOfDay(null); + return result; + } + + private static Boolean isTimeDurationTimex(String timex) { + return timex.startsWith(Constants.GENERAL_PERIOD_PREFIX.concat(Constants.TIME_TIMEX_PREFIX)); + } + + private static String getDurationTimexWithoutPrefix(String timex) { + // Remove "PT" prefix for TimeDuration, Remove "P" prefix for DateDuration + return timex.substring(isTimeDurationTimex(timex) ? 2 : 1); + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexInference.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexInference.java new file mode 100644 index 000000000..b7e8d11b6 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexInference.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.util.HashSet; + +public class TimexInference { + public static HashSet infer(TimexProperty timexProperty) { + HashSet types = new HashSet(); + + if (TimexInference.isPresent(timexProperty)) { + types.add(Constants.TimexTypes.PRESENT); + } + + if (TimexInference.isDefinite(timexProperty)) { + types.add(Constants.TimexTypes.DEFINITE); + } + + if (TimexInference.isDate(timexProperty)) { + types.add(Constants.TimexTypes.DATE); + } + + if (TimexInference.isDateRange(timexProperty)) { + types.add(Constants.TimexTypes.DATE_RANGE); + } + + if (TimexInference.isDuration(timexProperty)) { + types.add(Constants.TimexTypes.DURATION); + } + + if (TimexInference.isTime(timexProperty)) { + types.add(Constants.TimexTypes.TIME); + } + + if (TimexInference.isTimeRange(timexProperty)) { + types.add(Constants.TimexTypes.TIME_RANGE); + } + + if (types.contains(Constants.TimexTypes.PRESENT)) { + types.add(Constants.TimexTypes.DATE); + types.add(Constants.TimexTypes.TIME); + } + + if (types.contains(Constants.TimexTypes.TIME) && types.contains(Constants.TimexTypes.DURATION)) { + types.add(Constants.TimexTypes.TIME_RANGE); + } + + if (types.contains(Constants.TimexTypes.DATE) && types.contains(Constants.TimexTypes.TIME)) { + types.add(Constants.TimexTypes.DATE_TIME); + } + + if (types.contains(Constants.TimexTypes.DATE) && types.contains(Constants.TimexTypes.DURATION)) { + types.add(Constants.TimexTypes.DATE_RANGE); + } + + if (types.contains(Constants.TimexTypes.DATE_TIME) && types.contains(Constants.TimexTypes.DURATION)) { + types.add((Constants.TimexTypes.DATE_TIME_RANGE)); + } + + if (types.contains(Constants.TimexTypes.DATE) && types.contains(Constants.TimexTypes.TIME_RANGE)) { + types.add(Constants.TimexTypes.DATE_TIME_RANGE); + } + + return types; + } + + private static Boolean isPresent(TimexProperty timexProperty) { + return timexProperty.getNow() != null && timexProperty.getNow() == true; + } + + private static Boolean isDuration(TimexProperty timexProperty) { + return timexProperty.getYears() != null || timexProperty.getMonths() != null || timexProperty.getWeeks() != null || + timexProperty.getDays() != null | timexProperty.getHours() != null || + timexProperty.getMinutes() != null || timexProperty.getSeconds() != null; + } + + private static Boolean isTime(TimexProperty timexProperty) { + return timexProperty.getHour() != null && timexProperty.getMinute() != null && timexProperty.getSecond() != null; + } + + private static Boolean isDate(TimexProperty timexProperty) { + return timexProperty.getDayOfMonth() != null || timexProperty.getDayOfWeek() != null; + } + + private static Boolean isTimeRange(TimexProperty timexProperty) { + return timexProperty.getPartOfDay() != null; + } + + private static Boolean isDateRange(TimexProperty timexProperty) { + return (timexProperty.getDayOfMonth() == null && timexProperty.getDayOfWeek() == null) && + (timexProperty.getYear() != null || timexProperty.getMonth() != null || + timexProperty.getSeason() != null || timexProperty.getWeekOfYear() != null || + timexProperty.getWeekOfMonth() != null); + } + + private static Boolean isDefinite(TimexProperty timexProperty) { + return timexProperty.getYear() != null & timexProperty.getMonth() != null && timexProperty.getDayOfMonth() != null; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexParsing.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexParsing.java new file mode 100644 index 000000000..6692d755e --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexParsing.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.util.HashMap; +import java.util.Map; + +public class TimexParsing { + public static void parseString(String timex, TimexProperty timexProperty) { + // a reference to the present + if (timex == "PRESENT_REF") { + timexProperty.setNow(true); + } else if (timex.startsWith("P")) { + // duration + TimexParsing.extractDuration(timex, timexProperty); + } else if (timex.startsWith("(") && timex.endsWith(")")) { + // range indicated with start and end dates and a duration + TimexParsing.extractStartEndRange(timex, timexProperty); + } else { + // date andt ime and their respective ranges + TimexParsing.extractDateTime(timex, timexProperty); + } + } + + private static void extractDuration(String s, TimexProperty timexProperty) { + Map extracted = new HashMap(); + TimexRegex.extract("period", s, extracted); + timexProperty.assignProperties(extracted); + } + + private static void extractStartEndRange(String s, TimexProperty timexProperty) { + String[] parts = s.substring(1, s.length() - 1).split(","); + + if (parts.length == 3) { + TimexParsing.extractDateTime(parts[0], timexProperty); + TimexParsing.extractDuration(parts[2], timexProperty); + } + } + + private static void extractDateTime(String s, TimexProperty timexProperty) { + Integer indexOfT = s.indexOf("T"); + + if (indexOfT == -1) { + Map extracted = new HashMap(); + TimexRegex.extract("date", s, extracted); + timexProperty.assignProperties(extracted); + + } else { + Map extracted = new HashMap(); + TimexRegex.extract("date", s.substring(0, indexOfT), extracted); + TimexRegex.extract("time", s.substring(indexOfT), extracted); + timexProperty.assignProperties(extracted); + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexProperty.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexProperty.java new file mode 100644 index 000000000..97ac5691c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexProperty.java @@ -0,0 +1,445 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; + +public class TimexProperty { + private Time time; + + private String timexValue; + + private HashSet types; + + private Boolean now; + + private BigDecimal years; + + private BigDecimal months; + + private BigDecimal weeks; + + private BigDecimal days; + + private BigDecimal hours; + + private BigDecimal minutes; + + private BigDecimal seconds; + + private Integer year; + + private Integer month; + + private Integer dayOfMonth; + + private Integer dayOfWeek; + + private String season; + + private Integer weekOfYear; + + private Boolean weekend; + + public Integer weekOfMonth; + + private Integer hour; + + private Integer minute; + + private Integer second; + + private String partOfDay; + + public TimexProperty() { + + } + + public TimexProperty(String timex) { + TimexParsing.parseString(timex, this); + } + + public String getTimexValue() { + return TimexFormat.format(this); + } + + public void setTimexValue(String withTimexValue) { + this.timexValue = withTimexValue; + } + + public HashSet getTypes() { + return TimexInference.infer(this); + } + + public void setTypes(HashSet withTypes) { + this.types = withTypes; + } + + public Boolean getNow() { + return now; + } + + public void setNow(Boolean withNow) { + this.now = withNow; + } + + public BigDecimal getYears() { + return years; + } + + public void setYears(BigDecimal withYears) { + this.years = withYears; + } + + public BigDecimal getMonths() { + return months; + } + + public void setMonths(BigDecimal withMonths) { + this.months = withMonths; + } + + public BigDecimal getWeeks() { + return weeks; + } + + public void setWeeks(BigDecimal withWeeks) { + this.weeks = withWeeks; + } + + public BigDecimal getDays() { + return days; + } + + public void setDays(BigDecimal withDays) { + this.days = withDays; + } + + public BigDecimal getHours() { + return hours; + } + + public void setHours(BigDecimal withHours) { + this.hours = withHours; + } + + public BigDecimal getMinutes() { + return minutes; + } + + public void setMinutes(BigDecimal withMinutes) { + this.minutes = withMinutes; + } + + public BigDecimal getSeconds() { + return seconds; + } + + public void setSeconds(BigDecimal withSeconds) { + this.seconds = withSeconds; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer withYear) { + this.year = withYear; + } + + public Integer getMonth() { + return month; + } + + public void setMonth(Integer withMonth) { + this.month = withMonth; + } + + public Integer getDayOfMonth() { + return dayOfMonth; + } + + public void setDayOfMonth(Integer withDayOfMonth) { + this.dayOfMonth = withDayOfMonth; + } + + public Integer getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(Integer withDayOfWeek) { + this.dayOfWeek = withDayOfWeek; + } + + public String getSeason() { + return season; + } + + public void setSeason(String withSeason) { + this.season = withSeason; + } + + public Integer getWeekOfYear() { + return weekOfYear; + } + + public void setWeekOfYear(Integer withWeekOfYear) { + this.weekOfYear = withWeekOfYear; + } + + public Boolean getWeekend() { + return weekend; + } + + public void setWeekend(Boolean withWeekend) { + this.weekend = withWeekend; + } + + public Integer getWeekOfMonth() { + return weekOfMonth; + } + + public void setWeekOfMonth(Integer withWeekOfMonth) { + this.weekOfMonth = withWeekOfMonth; + } + + public Integer getHour() { + if (this.time != null) { + return this.time.getHour(); + } + + return null; + } + + public void setHour(Integer withHour) { + if (withHour != null) { + if (this.time == null) { + this.time = new Time(withHour, 0, 0); + } else { + this.time.setHour(withHour); + } + } else { + this.time = null; + } + } + + public Integer getMinute() { + if (this.time != null) { + return this.time.getMinute(); + } + + return null; + } + + public void setMinute(Integer withMinute) { + if (withMinute != null) { + if (this.time == null) { + time = new Time(0, withMinute, 0); + } else { + time.setMinute(withMinute); + } + } else { + this.time = null; + } + } + + public Integer getSecond() { + if (this.time != null) { + return this.time.getSecond(); + } + + return null; + } + + public void setSecond(Integer withSecond) { + if (withSecond != null) { + if (this.time == null) { + this.time = new Time(0, 0, withSecond); + } else { + this.time.setSecond(withSecond); + } + } else { + this.time = null; + } + } + + public String getPartOfDay() { + return partOfDay; + } + + public void setPartOfDay(String wthPartOfDay) { + this.partOfDay = wthPartOfDay; + } + + public static TimexProperty fromDate(LocalDateTime date) { + TimexProperty timex = new TimexProperty() { + { + setYear(date.getYear()); + setMonth(date.getMonthValue()); + setDayOfMonth(date.getDayOfMonth()); + } + }; + return timex; + } + + public static TimexProperty fromDateTime(LocalDateTime datetime) { + TimexProperty timex = TimexProperty.fromDate(datetime); + timex.setHour(datetime.getHour()); + timex.setMinute(datetime.getMinute()); + timex.setSecond(datetime.getSecond()); + return timex; + } + + public static TimexProperty fromTime(Time time) { + return new TimexProperty() { + { + setHour(time.getHour()); + setMinute(time.getMinute()); + setSecond(time.getSecond()); + } + }; + } + + @Override + public String toString() { + return TimexConvert.convertTimexToString(this); + } + + public String toNaturalLanguage(LocalDateTime referenceDate) { + return TimexRelativeConvert.convertTimexToStringRelative(this, referenceDate); + } + + public TimexProperty clone() { + Boolean now = this.getNow(); + BigDecimal years = this.getYears(); + BigDecimal months = this.getMonths(); + BigDecimal weeks = this.getWeeks(); + BigDecimal days = this.getDays(); + BigDecimal hours = this.getHours(); + BigDecimal minutes = this.getMinutes(); + BigDecimal seconds = this.getSeconds(); + Integer year = this.getYear(); + Integer month = this.getMonth(); + Integer dayOfMonth = this.getDayOfMonth(); + Integer dayOfWeek = this.getDayOfWeek(); + String season = this.getSeason(); + Integer weekOfYear = this.getWeekOfYear(); + Boolean weekend = this.getWeekend(); + Integer innerWeekOfMonth = this.getWeekOfMonth(); + Integer hour = this.getHour(); + Integer minute = this.getMinute(); + Integer second = this.getSecond(); + String partOfDay = this.getPartOfDay(); + + return new TimexProperty() { + { + setNow(now); + setYears(years); + setMonths(months); + setWeeks(weeks); + setDays(days); + setHours(hours); + setMinutes(minutes); + setSeconds(seconds); + setYear(year); + setMonth(month); + setDayOfMonth(dayOfMonth); + setDayOfWeek(dayOfWeek); + setSeason(season); + setWeekOfYear(weekOfYear); + setWeekend(weekend); + setWeekOfMonth(innerWeekOfMonth); + setHour(hour); + setMinute(minute); + setSecond(second); + setPartOfDay(partOfDay); + } + }; + } + + public void assignProperties(Map source) { + for (Entry item : source.entrySet()) { + + if (StringUtils.isBlank(item.getValue())) { + continue; + } + + switch (item.getKey()) { + case "year": + setYear(Integer.parseInt(item.getValue())); + break; + case "month": + setMonth(Integer.parseInt(item.getValue())); + break; + case "dayOfMonth": + setDayOfMonth(Integer.parseInt(item.getValue())); + break; + case "dayOfWeek": + setDayOfWeek(Integer.parseInt(item.getValue())); + break; + case "season": + setSeason(item.getValue()); + break; + case "weekOfYear": + setWeekOfYear(Integer.parseInt(item.getValue())); + break; + case "weekend": + setWeekend(true); + break; + case "weekOfMonth": + setWeekOfMonth(Integer.parseInt(item.getValue())); + break; + case "hour": + setHour(Integer.parseInt(item.getValue())); + break; + case "minute": + setMinute(Integer.parseInt(item.getValue())); + break; + case "second": + setSecond(Integer.parseInt(item.getValue())); + break; + case "partOfDay": + setPartOfDay(item.getValue()); + break; + case "dateUnit": + this.assignDateDuration(source); + break; + case "hourAmount": + setHours(new BigDecimal(item.getValue())); + break; + case "minuteAmount": + setMinutes(new BigDecimal(item.getValue())); + break; + case "secondAmount": + setSeconds(new BigDecimal(item.getValue())); + break; + default: + } + } + } + + private void assignDateDuration(Map source) { + switch (source.get("dateUnit")) { + case "Y": + this.years = new BigDecimal(source.get("amount")); + break; + case "M": + this.months = new BigDecimal(source.get("amount")); + break; + case "W": + this.weeks = new BigDecimal(source.get("amount")); + break; + case "D": + this.days = new BigDecimal(source.get("amount")); + break; + default: + } + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexRange.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexRange.java new file mode 100644 index 000000000..f15efd61a --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexRange.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +public class TimexRange { + private TimexProperty start; + + private TimexProperty end; + + private TimexProperty duration; + + public TimexProperty getStart() { + return start; + } + + public void setStart(TimexProperty withStart) { + this.start = withStart; + } + + public TimexProperty getEnd() { + return end; + } + + public void setEnd(TimexProperty withEnd) { + this.end = withEnd; + } + + public TimexProperty getDuration() { + return duration; + } + + public void setDuration(TimexProperty withDuration) { + this.duration = withDuration; + } +} diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexRangeResolver.java b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexRangeResolver.java new file mode 100644 index 000000000..c1f2bc76c --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/recognizers/text/expression/TimexRangeResolver.java @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.recognizers.datatypes.timex.expression; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class TimexRangeResolver { + public static List evaluate(Set candidates, List constraints) { + List timexConstraints = constraints.stream().map(x -> { + return new TimexProperty(x); + }).collect(Collectors.toList()); + Set candidatesWithDurationsResolved = TimexRangeResolver.resolveDurations(candidates, timexConstraints); + Set candidatesAccordingToDate = TimexRangeResolver + .resolveByDateRangeConstraints(candidatesWithDurationsResolved, timexConstraints); + Set candidatesWithAddedTime = TimexRangeResolver.resolveByTimeConstraints(candidatesAccordingToDate, + timexConstraints); + Set candidatesFilteredByTime = TimexRangeResolver.resolveByTimeRangeConstraints(candidatesWithAddedTime, + timexConstraints); + + List timexResults = candidatesFilteredByTime.stream().map(x -> { + return new TimexProperty(x); + }).collect(Collectors.toList()); + + return timexResults; + } + + public static Set resolveDurations(Set candidates, List constraints) { + Set results = new HashSet(); + for (String candidate : candidates) { + TimexProperty timex = new TimexProperty(candidate); + if (timex.getTypes().contains(Constants.TimexTypes.DURATION)) { + List r = TimexRangeResolver.resolveDuration(timex, constraints); + for (TimexProperty resolved : r) { + results.add(resolved.getTimexValue()); + } + } else { + results.add(candidate); + } + } + + return results; + } + + private static List resolveDuration(TimexProperty candidate, List constraints) { + List results = new ArrayList(); + for (TimexProperty constraint : constraints) { + if (constraint.getTypes().contains(Constants.TimexTypes.DATE_TIME)) { + results.add(TimexHelpers.timexDateTimeAdd(constraint, candidate)); + } else if (constraint.getTypes().contains(Constants.TimexTypes.TIME)) { + results.add(TimexHelpers.timexTimeAdd(constraint, candidate)); + } + } + + return results; + } + + private static Set resolveByDateRangeConstraints(Set candidates, + List timexConstraints) { + List dateRangeconstraints = timexConstraints.stream().filter(timex -> { + return timex.getTypes().contains(Constants.TimexTypes.DATE_RANGE); + }).map(timex -> { + return TimexHelpers.dateRangeFromTimex(timex); + }).collect(Collectors.toList()); + + List collapseDateRanges = TimexConstraintsHelper.collapseDateRanges(dateRangeconstraints); + + if (collapseDateRanges.isEmpty()) { + return candidates; + } + + List resolution = new ArrayList(); + for (String timex : candidates) { + List r = TimexRangeResolver.resolveDate(new TimexProperty(timex), collapseDateRanges); + resolution.addAll(r); + } + + return TimexRangeResolver.removeDuplicates(resolution); + } + + private static List resolveDate(TimexProperty timex, List constraints) { + List result = new ArrayList(); + for (DateRange constraint : constraints) { + result.addAll(TimexRangeResolver.resolveDateAgainstConstraint(timex, constraint)); + } + + return result; + } + + private static Set resolveByTimeRangeConstraints(Set candidates, + List timexConstrainst) { + List timeRangeConstraints = timexConstrainst.stream().filter(timex -> { + return timex.getTypes().contains(Constants.TimexTypes.TIME_RANGE); + }).map(timex -> { + return TimexHelpers.timeRangeFromTimex(timex); + }).collect(Collectors.toList()); + + List collapsedTimeRanges = TimexConstraintsHelper.collapseTimeRanges(timeRangeConstraints); + + if (collapsedTimeRanges.isEmpty()) { + return candidates; + } + + List resolution = new ArrayList(); + for (String timex : candidates) { + TimexProperty t = new TimexProperty(timex); + if (t.getTypes().contains(Constants.TimexTypes.TIME_RANGE)) { + List r = TimexRangeResolver.resolveTimeRange(t, collapsedTimeRanges); + resolution.addAll(r); + } else if (t.getTypes().contains(Constants.TimexTypes.TIME)) { + List r = TimexRangeResolver.resolveTime(t, collapsedTimeRanges); + resolution.addAll(r); + } + } + + return TimexRangeResolver.removeDuplicates(resolution); + } + + private static List resolveTimeRange(TimexProperty timex, List constraints) { + TimeRange candidate = TimexHelpers.timeRangeFromTimex(timex); + + List result = new ArrayList(); + for (TimeRange constraint : constraints) { + if (TimexConstraintsHelper.isOverlapping(candidate, constraint)) { + Integer start = Math.max(candidate.getStart().getTime(), constraint.getStart().getTime()); + Time time = new Time(start); + + // TODO: consider a method on TimexProperty to do this clone/overwrite pattern + TimexProperty resolved = timex.clone(); + resolved.setPartOfDay(null); + resolved.setSeconds(null); + resolved.setMinutes(null); + resolved.setHours(null); + resolved.setSecond(time.getSecond()); + resolved.setMinute(time.getMinute()); + resolved.setHour(time.getHour()); + + result.add(resolved.getTimexValue()); + } + } + + return result; + } + + private static List resolveTime(TimexProperty timex, List constraints) { + List result = new ArrayList(); + for (TimeRange constraint : constraints) { + result.addAll(TimexRangeResolver.resolveTimeAgainstConstraint(timex, constraint)); + } + + return result; + } + + private static List resolveTimeAgainstConstraint(TimexProperty timex, TimeRange constraint) { + Time t = new Time(timex.getHour(), timex.getMinute(), timex.getSecond()); + if (t.getTime() >= constraint.getStart().getTime() && t.getTime() < constraint.getEnd().getTime()) { + return new ArrayList() { + { + add(timex.getTimexValue()); + } + }; + } + + return new ArrayList(); + } + + private static Set removeDuplicates(List original) { + return new HashSet(original); + } + + private static List resolveDefiniteAgainstConstraint(TimexProperty timex, DateRange constraint) { + LocalDateTime timexDate = TimexHelpers.dateFromTimex(timex); + if (timexDate.compareTo(constraint.getStart()) >= 0 && timexDate.compareTo(constraint.getEnd()) < 0) { + return new ArrayList() { + { + add(timex.getTimexValue()); + } + }; + } + + return new ArrayList(); + } + + private static List resolveDateAgainstConstraint(TimexProperty timex, DateRange constraint) { + if (timex.getMonth() != null && timex.getDayOfMonth() != null) { + List result = new ArrayList(); + for (int year = constraint.getStart().getYear(); year <= constraint.getEnd() + .getYear(); year++) { + TimexProperty t = timex.clone(); + t.setYear(year); + result.addAll(TimexRangeResolver.resolveDefiniteAgainstConstraint(t, constraint)); + } + + return result; + } + + if (timex.getDayOfWeek() != null) { + // convert between ISO day of week and .NET day of week + DayOfWeek day = timex.getDayOfWeek() == 7 ? DayOfWeek.SUNDAY : DayOfWeek.of(timex.getDayOfWeek()); + List dates = TimexDateHelpers.datesMatchingDay(day, constraint.getStart(), constraint.getEnd()); + List result = new ArrayList(); + + for (LocalDateTime d : dates) { + TimexProperty t = timex.clone(); + t.setDayOfWeek(null); + t.setYear(d.getYear()); + t.setMonth(d.getMonthValue()); + t.setDayOfMonth(d.getDayOfMonth()); + result.add(t.getTimexValue()); + } + + return result; + } + + if (timex.getHour() != null) { + List result = new ArrayList(); + LocalDateTime day = constraint.getStart(); + while (day.compareTo(constraint.getEnd()) <= 0) { + TimexProperty t = timex.clone(); + t.setYear(day.getYear()); + t.setMonth(day.getMonthValue()); + t.setDayOfMonth(day.getDayOfMonth()); + result.addAll(TimexRangeResolver.resolveDefiniteAgainstConstraint(t, constraint)); + day = day.plusDays(1); + } + + return result; + } + + return new ArrayList(); + } + + private static Set resolveByTimeConstraints(Set candidates, List timexConstrainst) { + List