Browse Source

first commit

tags/2.0.1
Jonathan Cobb 4 years ago
commit
fa1150cb5d
100 changed files with 6734 additions and 0 deletions
  1. +11
    -0
      .gitignore
  2. +202
    -0
      LICENSE.txt
  3. +82
    -0
      pom.xml
  4. +35
    -0
      wizard-client/pom.xml
  5. +515
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/ApiClientBase.java
  6. +115
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiInnerScript.java
  7. +11
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiInnerScriptRunMode.java
  8. +50
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiMultiScriptDriver.java
  9. +505
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunner.java
  10. +33
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerListener.java
  11. +67
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerListenerBase.java
  12. +71
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerListenerStreamLogger.java
  13. +53
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerMultiListener.java
  14. +26
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerWithEnv.java
  15. +170
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScript.java
  16. +27
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeClasspathHandler.java
  17. +7
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeHandler.java
  18. +85
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptRequest.java
  19. +46
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptResponse.java
  20. +18
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptResponseCheck.java
  21. +7
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ListenerFunction.java
  22. +15
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/NamedApiConnectionInfo.java
  23. +181
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/client/script/SimpleApiRunnerListener.java
  24. +87
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/MainApiBase.java
  25. +67
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/MainApiOptionsBase.java
  26. +15
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/MainBase.java
  27. +55
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/ModelSetupMainBase.java
  28. +57
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/ModelSetupOptionsBase.java
  29. +372
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/ScriptMainBase.java
  30. +120
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/main/ScriptMainOptionsBase.java
  31. +30
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ManifestClasspathResolver.java
  32. +24
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ManifestFileResolver.java
  33. +24
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelDiffEntry.java
  34. +15
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelEntity.java
  35. +26
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelManifestResolver.java
  36. +103
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelMigration.java
  37. +8
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelMigrationListener.java
  38. +644
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelSetup.java
  39. +23
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelSetupListener.java
  40. +27
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelSetupListenerBase.java
  41. +21
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/ModelVerifyLog.java
  42. +164
    -0
      wizard-client/src/main/java/org/cobbzilla/wizard/model/entityconfig/StandardModelVerifyLog.java
  43. +41
    -0
      wizard-client/src/main/resources/org/cobbzilla/wizard/model/entityconfig/model_verify_template.html.hbs
  44. +103
    -0
      wizard-common/pom.xml
  45. +30
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/AnalyticsConfiguration.java
  46. +5
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/AnalyticsData.java
  47. +19
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/AnalyticsDataBase.java
  48. +12
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/AnalyticsHandler.java
  49. +12
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/AnalyticsHandlerBase.java
  50. +36
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/influxdb/InfluxData.java
  51. +18
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/analytics/influxdb/InfluxDataHandler.java
  52. +24
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/api/ApiException.java
  53. +13
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/api/CrudOperation.java
  54. +14
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/api/ForbiddenException.java
  55. +11
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/api/NotFoundException.java
  56. +40
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/api/ValidationException.java
  57. +13
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/asset/AssetStorageConfiguration.java
  58. +11
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/asset/AssetStorageType.java
  59. +24
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/auth/AuthResponse.java
  60. +20
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/auth/AuthenticationException.java
  61. +26
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/auth/ChangePasswordRequest.java
  62. +50
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/auth/LoginRequest.java
  63. +15
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/auth/ResetPasswordRequest.java
  64. +21
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/ldap/FirstDnPartValueProducer.java
  65. +32
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/ldap/LdapUtil.java
  66. +19
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/ApiToken.java
  67. +8
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/AssetStorageInfo.java
  68. +40
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/AssetStorageInfoBase.java
  69. +99
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/AuditLog.java
  70. +33
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/BasicAccount.java
  71. +33
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/BasicConstraintConstants.java
  72. +29
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/ChildEntity.java
  73. +43
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/EntityMask.java
  74. +7
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/ExcludeUpdates.java
  75. +12
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/ExpirableBase.java
  76. +35
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/FilterableSqlViewSearchResult.java
  77. +7
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/HasRelatedEntities.java
  78. +71
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/HashedPassword.java
  79. +59
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/Identifiable.java
  80. +140
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/IdentifiableBase.java
  81. +15
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/NamedEntity.java
  82. +71
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/NamedIdentityBase.java
  83. +11
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/ParentAndSuccessor.java
  84. +72
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/RSBean.java
  85. +18
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/RelatedEntities.java
  86. +84
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/SemanticVersion.java
  87. +23
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/StrongIdentifiableBase.java
  88. +37
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/UniqueEmailEntity.java
  89. +43
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/UniquelyNamedEntity.java
  90. +16
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/context/ContextEntry.java
  91. +15
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/context/ContextEntryNode.java
  92. +100
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/context/SavedContext.java
  93. +45
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/crypto/EncryptedBoolean.java
  94. +26
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/crypto/EncryptedTypes.java
  95. +27
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/crypto/package-info.java
  96. +640
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfig.java
  97. +7
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigFieldToObject.java
  98. +14
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigFieldValidator.java
  99. +7
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigSource.java
  100. +24
    -0
      wizard-common/src/main/java/org/cobbzilla/wizard/model/entityconfig/EntityConfigValidator.java

+ 11
- 0
.gitignore View File

@@ -0,0 +1,11 @@
dependency-reduced-pom.xml
target
.idea
*.iml
logs
build.log
*-merged.yml
tmp
build.log
*~
.DS_Store

+ 202
- 0
LICENSE.txt View File

@@ -0,0 +1,202 @@

Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.

"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:

(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and

(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and

(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and

(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.

You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

cobbzilla-wizard is Copyright 2013 Jonathan Cobb

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

+ 82
- 0
pom.xml View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
(c) Copyright 2013-2014 Jonathan Cobb
cobbzilla-wizard is available under the Apache License, version 2: http://www.apache.org/licenses/LICENSE-2.0.html
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.cobbzilla</groupId>
<artifactId>cobbzilla-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

<artifactId>cobbzilla-wizard</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>

<modules>
<module>wizard-common</module>
<module>wizard-server</module>
<module>wizard-client</module>
<module>wizard-server-test</module>
</modules>

<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>

<dependencies>

<!-- Hibernate, but only so the annotations don't make the compiler barf -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>

<!-- handy stuff -->
<dependency>
<groupId>org.cobbzilla</groupId>
<artifactId>cobbzilla-utils</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

<!-- JSR 303 with Hibernate Validator -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javassist.version}</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>${cglib.version}</version>
</dependency>

</dependencies>

<build>
<plugins>
<!-- use Java 11 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>11</source>
<target>11</target>
<showWarnings>true</showWarnings>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

</project>

+ 35
- 0
wizard-client/pom.xml View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
(c) Copyright 2013-2014 Jonathan Cobb
This code is available under the Apache License, version 2: http://www.apache.org/licenses/LICENSE-2.0.html
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.cobbzilla</groupId>
<artifactId>cobbzilla-wizard</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

<artifactId>wizard-client</artifactId>
<version>1.0.0-SNAPSHOT</version>

<dependencies>

<dependency>
<groupId>org.cobbzilla</groupId>
<artifactId>wizard-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.cobbzilla</groupId>
<artifactId>cobbzilla-utils</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

</dependencies>

</project>

+ 515
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/ApiClientBase.java View File

@@ -0,0 +1,515 @@
package org.cobbzilla.wizard.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.SingletonMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.cobbzilla.util.collection.NameAndValue;
import org.cobbzilla.util.http.ApiConnectionInfo;
import org.cobbzilla.util.http.HttpRequestBean;
import org.cobbzilla.util.http.HttpResponseBean;
import org.cobbzilla.util.http.HttpUtil;
import org.cobbzilla.util.reflect.ReflectionUtil;
import org.cobbzilla.wizard.api.ApiException;
import org.cobbzilla.wizard.api.ForbiddenException;
import org.cobbzilla.wizard.api.NotFoundException;
import org.cobbzilla.wizard.api.ValidationException;
import org.cobbzilla.wizard.model.entityconfig.ModelEntity;
import org.cobbzilla.wizard.util.RestResponse;

import java.io.*;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.TimeUnit;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;
import static org.apache.http.HttpHeaders.LOCATION;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_OCTET_STREAM;
import static org.cobbzilla.util.http.HttpMethods.*;
import static org.cobbzilla.util.http.HttpStatusCodes.*;
import static org.cobbzilla.util.io.FileUtil.getDefaultTempDir;
import static org.cobbzilla.util.json.JsonUtil.fromJson;
import static org.cobbzilla.util.json.JsonUtil.toJson;
import static org.cobbzilla.util.reflect.ReflectionUtil.closeQuietly;
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate;
import static org.cobbzilla.util.string.StringUtil.UTF8cs;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.util.time.TimeUtil.formatDuration;
import static org.cobbzilla.wizard.model.Identifiable.ENTITY_TYPE_HEADER_NAME;

@Slf4j @NoArgsConstructor @ToString(of={"connectionInfo"})
public class ApiClientBase implements Cloneable, Closeable {

public static final ContentType CONTENT_TYPE_JSON = ContentType.APPLICATION_JSON;
public static final long INITIAL_RETRY_DELAY = TimeUnit.SECONDS.toMillis(1);

@SuppressWarnings("CloneDoesntCallSuperClone") // subclasses must have a copy constructor
@Override public Object clone() { return instantiate(getClass(), this); }

@Getter @Setter protected ApiConnectionInfo connectionInfo;
@Getter protected String token;

public String getSuperuserToken () { return null; } // subclasses may override

@Getter @Setter protected String entityTypeHeaderName = ENTITY_TYPE_HEADER_NAME;
public boolean hasEntityTypeHeaderName () { return !empty(entityTypeHeaderName); }

// the server may be coming up, and either not accepting connections or issuing 503 Service Unavailable.
@Getter @Setter protected int numTries = 5;
@Getter @Setter protected long retryDelay = INITIAL_RETRY_DELAY;

@Getter @Setter protected boolean captureHeaders = false;
@Getter @Setter private HttpContext httpContext = null;
@Getter private Map<String, String> headers = null;

public void setHeaders(JsonNode jsonNode) {
final ObjectMapper mapper = new ObjectMapper();
headers = mapper.convertValue(jsonNode, Map.class);
}
public void removeHeaders () { headers = null; }

public void setToken(String token) {
this.token = token;
this.tokenCtime = empty(token) ? 0 : now();
}

private long tokenCtime = 0;
public boolean hasToken () { return !empty(token); }
public long getTokenAge () { return now() - tokenCtime; }

public static final int CONNECT_TIMEOUT = (int) SECONDS.toMillis(10);
public static final int SOCKET_TIMEOUT = (int) SECONDS.toMillis(60);
public static final int REQUEST_TIMEOUT = (int) SECONDS.toMillis(60);

@Getter @Setter private int connectTimeout = CONNECT_TIMEOUT;
@Getter @Setter private int socketTimeout = SOCKET_TIMEOUT;
@Getter @Setter private int requestTimeout = REQUEST_TIMEOUT;

public void logout () { setToken(null); }

public ApiClientBase (ApiClientBase other) { this.connectionInfo = other.getConnectionInfo(); }
public ApiClientBase (ApiConnectionInfo connectionInfo) { this.connectionInfo = connectionInfo; }
public ApiClientBase (String baseUri) { connectionInfo = new ApiConnectionInfo(baseUri); }

public ApiClientBase (ApiConnectionInfo connectionInfo, HttpClient httpClient) {
this(connectionInfo);
setHttpClient(httpClient);
}

public ApiClientBase (String baseUri, HttpClient httpClient) {
this(baseUri);
setHttpClient(httpClient);
}

public String getBaseUri () { return connectionInfo.getBaseUri(); }

protected HttpClient httpClient;
public HttpClient getHttpClient() {
if (httpClient == null) {
final RequestConfig.Builder requestBuilder = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setSocketTimeout(socketTimeout)
.setConnectionRequestTimeout(requestTimeout);

httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestBuilder.build())
.build();
}
return httpClient;
}
public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; }

public RestResponse process(HttpRequestBean requestBean) throws Exception {
switch (requestBean.getMethod()) {
case GET:
return doGet(requestBean.getUri());
case POST:
return doPost(requestBean.getUri(), getJson(requestBean));
case PUT:
return doPut(requestBean.getUri(), getJson(requestBean));
case DELETE:
return doDelete(requestBean.getUri());
default:
return die("Unsupported request method: "+requestBean.getMethod());
}
}

public RestResponse process_raw(HttpRequestBean requestBean) throws Exception {
switch (requestBean.getMethod()) {
case GET:
return doGet(requestBean.getUri());
case POST:
return doPost(requestBean.getUri(), requestBean.getEntity(), requestBean.getContentType());
case PUT:
return doPut(requestBean.getUri(), requestBean.getEntity(), requestBean.getContentType());
case DELETE:
return doDelete(requestBean.getUri());
default:
return die("Unsupported request method: "+requestBean.getMethod());
}
}

protected void assertStatusOK(RestResponse response) {
if (response.status != OK
&& response.status != CREATED
&& response.status != NO_CONTENT) throw new ApiException(response);
}

protected String getJson(HttpRequestBean requestBean) throws Exception {
Object data = requestBean.getEntity();
if (data == null) return null;
if (data instanceof String) return (String) data;
return toJson(data);
}

protected ApiException specializeApiException(ApiException e) { return specializeApiException(null, e.getResponse()); }

protected ApiException specializeApiException(HttpRequestBean request, RestResponse response) {
if (response.isSuccess()) {
die("specializeApiException: cannot specialize exception for a successful response: "+response);
}
switch (response.status) {
case NOT_FOUND:
return new NotFoundException(request, response);
case FORBIDDEN:
return new ForbiddenException(request, response);
case UNPROCESSABLE_ENTITY:
return new ValidationException(request, response);
default: return new ApiException(request, response);
}
}

public RestResponse doGet(String path) throws Exception {
final HttpClient client = getHttpClient();
final String url = getUrl(path, getBaseUri());
@Cleanup("releaseConnection") HttpGet httpGet = new HttpGet(url);
return getResponse(client, httpGet);
}

public RestResponse get(String path) throws Exception {
final RestResponse restResponse = doGet(path);
if (!restResponse.isSuccess()) throw specializeApiException(HttpRequestBean.get(path), restResponse);
return restResponse;
}

public <T> T get(String path, Class<T> responseClass) throws Exception {
return fromJson(get(path).json, responseClass);
}

protected <T> void setRequestEntity(HttpEntityEnclosingRequest entityRequest, T data, ContentType contentType) {
if (data != null) {
if (data instanceof InputStream) {
entityRequest.setEntity(new InputStreamEntity((InputStream) data, contentType));
log.debug("setting entity=(InputStream)");
} else {
entityRequest.setEntity(new StringEntity(data.toString(), contentType));
log.debug("setting entity=(" + data.toString().length()+" json chars)");
log.trace(data.toString());
}
}
}

public RestResponse doPost(String path, String json) throws Exception {
return doPost(path, json, CONTENT_TYPE_JSON);
}

public <T> RestResponse doPost(String path, T data, ContentType contentType) throws Exception {
final HttpClient client = getHttpClient();
final String url = getUrl(path, getBaseUri());
@Cleanup("releaseConnection") HttpPost httpPost = new HttpPost(url);
setRequestEntity(httpPost, data, contentType);
return getResponse(client, httpPost);
}

public <T> T post(String path, Object request, Class<T> responseClass) throws Exception {
if (request instanceof String) return post(path, request, responseClass);
if (request instanceof ModelEntity) return post(path, ((ModelEntity) request).getEntity(), responseClass);
return fromJson(post(path, toJson(request)).json, responseClass);
}

public RestResponse doPost(String path, File uploadFile) throws Exception {
return uploadFile(path, uploadFile, POST);
}

public RestResponse doPut(String path, File uploadFile) throws Exception {
return uploadFile(path, uploadFile, PUT);
}

private RestResponse uploadFile(String path, File uploadFile, String method) throws Exception {
final String url = getUrl(path, getBaseUri());
final NameAndValue[] headers = { new NameAndValue(getTokenHeader(), token) };

@Cleanup final InputStream in = new FileInputStream(uploadFile);
final HttpRequestBean request = new HttpRequestBean(method, url, in, uploadFile.getName(), headers);
final HttpResponseBean response = HttpUtil.getStreamResponse(request);

return new RestResponse(response);
}

public <T> T post(String path, T request) throws Exception {
if (request instanceof ModelEntity) {
return (T) post(path, ((ModelEntity) request).getEntity(), ((ModelEntity) request).getEntity().getClass());
} else {
return post(path, request, (Class<T>) request.getClass());
}
}

public RestResponse post(String path, String json) throws Exception {
return post(path, json, CONTENT_TYPE_JSON);
}

public RestResponse post(String path, String data, ContentType contentType) throws Exception {
final RestResponse restResponse = doPost(path, data, contentType);
if (!restResponse.isSuccess()) throw specializeApiException(HttpRequestBean.post(path, data), restResponse);
return restResponse;
}

public RestResponse put(String path, String data, ContentType contentType) throws Exception {
final RestResponse restResponse = doPut(path, data, contentType);
if (!restResponse.isSuccess()) throw specializeApiException(HttpRequestBean.put(path, data), restResponse);
return restResponse;
}

public RestResponse doPut(String path, String json) throws Exception {
return doPut(path, json, CONTENT_TYPE_JSON);
}

public <T> RestResponse doPut(String path, T data, ContentType contentType) throws Exception {
HttpClient client = getHttpClient();
final String url = getUrl(path, getBaseUri());
@Cleanup("releaseConnection") HttpPut httpPut = new HttpPut(url);
setRequestEntity(httpPut, data, contentType);
return getResponse(client, httpPut);
}

public <T> T put(String path, Object request, Class<T> responseClass) throws Exception {
return fromJson(put(path, toJson(request)).json, responseClass);
}

public <T> T put(String path, T request) throws Exception {
if (request instanceof ModelEntity) {
return (T) put(path, ((ModelEntity) request).getEntity(), ((ModelEntity) request).getEntity().getClass());
} else {
return put(path, request, (Class<T>) request.getClass());
}
}

public <T> T put(String path, String json, Class<T> responseClass) throws Exception {
final RestResponse response = put(path, json);
if (!response.isSuccess()) throw specializeApiException(HttpRequestBean.put(path, json), response);
return fromJson(response.json, responseClass);
}

public RestResponse put(String path, String json) throws Exception {
final RestResponse restResponse = doPut(path, json);
if (!restResponse.isSuccess()) throw specializeApiException(HttpRequestBean.put(path, json), restResponse);
return restResponse;
}

public RestResponse doDelete(String path) throws Exception {
HttpClient client = getHttpClient();
final String url = getUrl(path, getBaseUri());
@Cleanup("releaseConnection") HttpDelete httpDelete = new HttpDelete(url);
return getResponse(client, httpDelete);
}

public RestResponse delete(String path) throws Exception {
final RestResponse restResponse = doDelete(path);
if (!restResponse.isSuccess()) throw specializeApiException(HttpRequestBean.delete(path), restResponse);
return restResponse;
}

private String getUrl(String path, String clientUri) {
if (path.startsWith("http://") || path.startsWith("https://")) {
return path; // caller has supplied an absolute path

} else if (path.startsWith("/") && clientUri.endsWith("/")) {
path = path.substring(1); // caller has supplied a relative path
}
return clientUri + path;
}

public RestResponse getResponse(HttpClient client, HttpRequestBase request) throws IOException {
request = beforeSend(request);
RestResponse restResponse = null;
IOException exception = null;
retryDelay = INITIAL_RETRY_DELAY;
for (int i=0; i<numTries; i++) {
if (i > 0) {
sleep(retryDelay);
retryDelay *= 2;
}
try {
final HttpResponse response = execute(client, request);
final int statusCode = response.getStatusLine().getStatusCode();
String responseJson = null;
byte[] responseBytes = null;
final HttpEntity entity = response.getEntity();
if (entity != null) {
try (InputStream in = entity.getContent()) {
if (isCaptureHeaders() && response.containsHeader("content-disposition")) {
responseBytes = IOUtils.toByteArray(in);
} else {
responseJson = IOUtils.toString(in, UTF8cs);
log.debug("response: " + responseJson);
}
}
} else {
responseJson = null;
}

restResponse = empty(responseBytes)
? new RestResponse(statusCode, responseJson, getLocationHeader(response))
: new RestResponse(statusCode, responseBytes, getLocationHeader(response));
if (isCaptureHeaders() || hasEntityTypeHeaderName()) {
for (Header header : response.getAllHeaders()) {
if (isCaptureHeaders() || header.getName().equals(getEntityTypeHeaderName())) {
restResponse.addHeader(header.getName(), header.getValue());
}
}
}
if (statusCode != SERVER_UNAVAILABLE) return restResponse;
log.warn("getResponse("+request.getMethod()+" "+request.getURI().toASCIIString()+", attempt="+i+"/"+numTries+") returned "+SERVER_UNAVAILABLE+", will " + ((i+1)>=numTries ? "NOT":"sleep for "+formatDuration(retryDelay)+" then") + " retry the request");

} catch (IOException e) {
log.warn("getResponse("+request.getMethod()+" "+request.getURI().toASCIIString()+", attempt="+i+"/"+numTries+") threw exception "+e+", will " + ((i+1)>=numTries ? "NOT":"sleep for "+formatDuration(retryDelay)+" then") + " retry the request");
exception = e;
}
}
if (restResponse != null) return restResponse;
if (exception == null) return die("getResponse: unknown error");
throw exception;
}

public HttpResponse execute(HttpClient client, HttpRequestBase request) throws IOException {
return client.execute(request, httpContext);
}

public File getFile (String path) throws IOException {

final HttpClient client = getHttpClient();
final String url = getUrl(path, getBaseUri());
@Cleanup("releaseConnection") HttpRequestBase request = new HttpGet(url);
request = beforeSend(request);

final HttpResponse response = client.execute(request);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == NOT_FOUND) return null;
if (statusCode == FORBIDDEN) throw new ForbiddenException();
if (!RestResponse.isSuccess(statusCode)) die("getFile("+url+"): error: "+statusCode);

final HttpEntity entity = response.getEntity();
if (entity == null) die("getFile("+url+"): No entity");

final File file = File.createTempFile(getClass().getName()+"-", getTempFileSuffix(path, HttpUtil.getContentType(response)), getDefaultTempDir());
try (InputStream in = entity.getContent()) {
try (OutputStream out = new FileOutputStream(file)) {
IOUtils.copyLarge(in, out);
}
}

return file;
}

protected String getTempFileSuffix(String path, String contentType) {
if (empty(contentType)) return ".temp";
switch (contentType) {
case "image/jpeg": return ".jpg";
case "image/gif": return ".gif";
case "image/png": return ".png";
default: return ".temp";
}
}

protected HttpRequestBase beforeSend(HttpRequestBase request) {
if (!empty(token)) {
final String tokenHeader = getTokenHeader();
if (empty(tokenHeader)) die("token set but getTokenHeader returned null");
request.addHeader(tokenHeader, token);
}
final Map<String, String> headers = getHeaders();
if (!empty(headers)) {
for (Map.Entry<String, String> entry: headers.entrySet()){
request.addHeader(entry.getKey(), entry.getValue());
}
}
return request;
}

public String getTokenHeader() { return null; }

protected final Stack<String> tokenStack = new Stack<>();
public void pushToken(String token) {
synchronized (tokenStack) {
if (tokenStack.isEmpty()) tokenStack.push(getToken());
tokenStack.push(token);
setToken(token);
}
}

/**
* Pops the current token off the stack.
* Now the top of the stack is the previous token, so it becomes the active one.
* @return The current token popped off (NOT the current active token, call getToken() to get that)
*/
public String popToken() {
synchronized (tokenStack) {
tokenStack.pop();
setToken(tokenStack.peek());
return getToken();
}
}

public static final String LOCATION_HEADER = LOCATION;
private String getLocationHeader(HttpResponse response) {
final Header header = response.getFirstHeader(LOCATION_HEADER);
return header == null ? null : header.getValue();
}

// call our own copy constructor, return new instance that is a copy of ourselves
public ApiClientBase copy() { return ReflectionUtil.copy(this); }

public HttpResponseBean getResponse(HttpRequestBean request) throws IOException {
if (!request.hasHeader(CONTENT_TYPE)) request.setHeader(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
final HttpRequestBean realRequest = new HttpRequestBean(request);
realRequest.setUri(getBaseUri()+request.getUri());
realRequest.setHeader(getTokenHeader(), getToken());
return HttpUtil.getResponse(realRequest);
}

public InputStream getStream(HttpRequestBean request) throws IOException {
return HttpUtil.get(getBaseUri()+request.getUri(), new SingletonMap<>(getTokenHeader(), getToken()));
}

public String getStreamedString(HttpRequestBean request) throws IOException {
try {
@Cleanup InputStream in = getStream(request);
if (in == null) throw new NotFoundException(request, null);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copyLarge(in, out);
return new String(out.toByteArray());
} catch (FileNotFoundException e) {
throw new NotFoundException(request, null);
}
}

@Override public void close() { closeQuietly(httpClient); }

}

+ 115
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiInnerScript.java View File

@@ -0,0 +1,115 @@
package org.cobbzilla.wizard.client.script;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.github.jknack.handlebars.Handlebars;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.javascript.JsEngine;

import java.util.*;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.json.JsonUtil.json;

@Slf4j
public class ApiInnerScript {

@Getter @Setter private ApiScript parent;
@Getter @Setter private ApiInnerScriptRunMode runMode = ApiInnerScriptRunMode.fail_fast;

@Getter @Setter private Map<String, String> params;
public boolean hasParams () { return !empty(params); }
public void setParam(String name, String value) {
if (params == null) params = new HashMap<>();
params.put(name, value);
}

@Getter @Setter private char paramStartDelim = '[';
@Getter @Setter private char paramEndDelim = ']';

@Getter @Setter private Map<String, String> iterate;
public boolean hasIterate () { return !empty(iterate); }
public void setIterate(String name, String value) {
if (iterate == null) iterate = new HashMap<>();
iterate.put(name, value);
}

@Getter @Setter private ApiScript[] scripts;
public boolean hasScripts () { return !empty(scripts); }

public List<ApiScript> getAllScripts (JsEngine js, Handlebars handlebars, Map<String, Object> ctx) {
if (!hasScripts()) {
log.warn("getAllScripts: no scripts!");
return Collections.emptyList();
}
final List<ApiScript> all = new ArrayList<>();
if (hasParams()) {
for (Map.Entry<String, String> param : getParams().entrySet()) {
ctx.put(param.getKey(), js.evaluate(param.getValue(), ctx));
}
}
if (hasIterate()) {
final Map<String, String> iterations = new HashMap<>(getIterate());
applyIterations(js, handlebars, ctx, iterations, all);
} else {
all.addAll(buildScripts(handlebars, ctx));
}
return all;
}

private void applyIterations(JsEngine js, Handlebars handlebars, Map<String, Object> ctx, Map<String, String> iterations, List<ApiScript> scripts) {
if (empty(iterations)) {
scripts.addAll(buildScripts(handlebars, ctx));
return;
}
final Map<String, Object> ctxCopy = new HashMap<>(ctx);

final Iterator<Map.Entry<String, String>> iter = iterations.entrySet().iterator();
final Map.Entry<String, String> entry = iter.next();
iter.remove();

final Object var = js.evaluate(entry.getValue(), ctxCopy);
if (empty(var)) {
log.warn("applyIterations: var "+entry.getValue()+" evaluated to something empty: "+var);
} else {
final List<Object> resolved = new ArrayList<>();
if (var.getClass().isArray()) {
resolved.addAll(Arrays.asList((Object[]) var));
} else if (var instanceof Collection) {
resolved.addAll((Collection<?>) var);
} else if (var instanceof ScriptObjectMirror && ((ScriptObjectMirror) var).isArray()) {
final Object[] objects = ((ScriptObjectMirror) var).to(Object[].class);
if (objects.length == 0) {
log.warn("applyIterations: var "+entry.getValue()+" was an empty array");
return;
}
if (objects[0] instanceof ScriptObjectMirror) {
resolved.addAll(Arrays.asList(((ScriptObjectMirror) var).to(Map[].class)));
} else {
resolved.addAll(Arrays.asList(objects));
}
} else {
resolved.add(var);
}
for (Object value : resolved) {
ctxCopy.put(entry.getKey(), value);
applyIterations(js, handlebars, ctxCopy, iterations, scripts);
}
}
}

private List<ApiScript> buildScripts(Handlebars handlebars, Map<String, Object> ctx) {
final List<ApiScript> scripts = new ArrayList<>();
for (ApiScript script : getCopyOfScripts()) {
HandlebarsUtil.applyReflectively(handlebars, script, ctx, paramStartDelim, paramEndDelim);
scripts.add(script);
}
return scripts;
}

@JsonIgnore private ApiScript[] getCopyOfScripts() { return json(json(getScripts()), ApiScript[].class); }

}

+ 11
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiInnerScriptRunMode.java View File

@@ -0,0 +1,11 @@
package org.cobbzilla.wizard.client.script;

import com.fasterxml.jackson.annotation.JsonCreator;

public enum ApiInnerScriptRunMode {

fail_fast, run_all, run_all_verbose_errors;

@JsonCreator public static ApiInnerScriptRunMode fromString (String val) { return valueOf(val.toLowerCase()); }

}

+ 50
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiMultiScriptDriver.java View File

@@ -0,0 +1,50 @@
package org.cobbzilla.wizard.client.script;

import com.github.jknack.handlebars.Handlebars;
import lombok.*;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.cobbzilla.util.collection.multi.MultiResultDriverBase;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.reflect.ObjectFactory;
import org.cobbzilla.util.reflect.ReflectionUtil;

import java.util.Map;

@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true) @Slf4j
public class ApiMultiScriptDriver extends MultiResultDriverBase {

@Getter @Setter private ApiRunner apiRunner;
@Getter @Setter private Handlebars handlebars;
@Getter @Setter private String testTemplate;
@Getter @Setter private ObjectFactory<CloseableHttpClient> httpClientFactory;

@Getter private Map<String, Object> context;
@Override public void setContext(Map<String, Object> context) { this.context = context; }

@Getter @Setter private int maxConcurrent;
@Getter @Setter private long timeout;

@Override public void before() { apiRunner.reset(); }

protected String getTestName(Object task) { return ReflectionUtil.get(task, "name", ""+task.hashCode()); }

@Override protected String successMessage(Object task) { return getTestName(task); }
@Override protected String failureMessage(Object task) { return getTestName(task); }

protected boolean resetApiContext() { return true; }

@Override protected void run(Object task) throws Exception {
@Cleanup final CloseableHttpClient httpClient = httpClientFactory.create();
final ApiRunner api = new ApiRunner(apiRunner, httpClient);
if (resetApiContext()) api.getContext().clear();
api.run(taskToScript(task));
}

protected String taskToScript(Object task) {
final Map<String, Object> ctx = ReflectionUtil.toMap(task);
return HandlebarsUtil.apply(handlebars, testTemplate, ctx);
}

}

+ 505
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunner.java View File

@@ -0,0 +1,505 @@
package org.cobbzilla.wizard.client.script;

import com.fasterxml.jackson.databind.JsonNode;
import com.github.jknack.handlebars.Handlebars;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.HttpClientContext;
import org.cobbzilla.util.collection.NameAndValue;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.handlebars.SimpleJurisdictionResolver;
import org.cobbzilla.util.http.HttpMethods;
import org.cobbzilla.util.http.HttpStatusCodes;
import org.cobbzilla.util.io.FileUtil;
import org.cobbzilla.util.javascript.StandardJsEngine;
import org.cobbzilla.util.string.StringUtil;
import org.cobbzilla.wizard.client.ApiClientBase;
import org.cobbzilla.wizard.util.RestResponse;
import org.cobbzilla.wizard.util.TestNames;
import org.cobbzilla.wizard.validation.ConstraintViolationBean;
import org.cobbzilla.wizard.validation.ValidationErrors;

import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static org.apache.http.HttpHeaders.CONTENT_TYPE;
import static org.apache.http.entity.ContentType.MULTIPART_FORM_DATA;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpStatusCodes.OK;
import static org.cobbzilla.util.json.JsonUtil.*;
import static org.cobbzilla.util.reflect.ReflectionUtil.copy;
import static org.cobbzilla.util.reflect.ReflectionUtil.forName;
import static org.cobbzilla.util.string.StringUtil.trimQuotes;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.wizard.client.script.ApiScript.DEFAULT_SESSION_NAME;
import static org.cobbzilla.wizard.client.script.ApiScript.PARAM_REQUIRED;

@NoArgsConstructor @Accessors(chain=true) @Slf4j
public class ApiRunner {

public static final String CTX_JSON = "json";
public static final String CTX_RESPONSE = "response";
public static final String NEW_SESSION = "new";
public static final String TEMPORAL_NEW_SESSION = "temp-new";

private StandardJsEngine js = new StandardJsEngine();

@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") // intended for use in debugging
@Getter private static Map<String, ApiScript> currentScripts = new HashMap<>();

public ApiRunner(ApiClientBase api, ApiRunnerListener listener) {
this.api = api;
this.listener = listener;
this.listener.setApiRunner(this);
}

public ApiRunner(ApiRunner other, HttpClient httpClient) {
copy(this, other);
this.api = other.api.copy();
this.api.setHttpClient(httpClient);
this.api.setHttpContext(HttpClientContext.create());
this.listener = copy(other.listener);
this.listener.setApiRunner(this);
this.ctx.putAll(other.getContext());
}

public ApiRunner(StandardJsEngine js, ApiClientBase api, ApiRunnerListener listener, ApiScriptIncludeHandler includeHandler) {
this.js = js;
this.api = api;
this.listener = listener;
this.listener.setApiRunner(this);
this.includeHandler = includeHandler;
}

public void setScriptForThread(ApiScript script) {
if (script.hasComment()) log.info(script.getComment());
currentScripts.put(Thread.currentThread().getName(), script);
}

// be careful - if you have multiple ApiRunners in the same classloader, these methods will not be useful
// intended for convenience in the common case of a single ApiRunner
public static ApiScript script() { return currentScripts.isEmpty() ? null : currentScripts.values().iterator().next(); }
public static String comment() { return currentScripts.isEmpty() ? null : currentScripts.values().iterator().next().getComment(); }

@Getter private ApiClientBase api;
@Getter private ApiClientBase currentApi;

private Map<String, ApiClientBase> alternateApis = new HashMap<>();

private ApiRunnerListener listener;
@Getter @Setter private ApiScriptIncludeHandler includeHandler;

protected final Map<String, Object> ctx = new ConcurrentHashMap<>();
public Map<String, Object> getContext () { return ctx; }

@Getter(lazy=true) private final Handlebars handlebars = initHandlebars();
protected Handlebars initHandlebars() {
final Handlebars hb = new Handlebars(new HandlebarsUtil("api-runner(" + api + ")"));
HandlebarsUtil.registerUtilityHelpers(hb);
HandlebarsUtil.registerCurrencyHelpers(hb);
HandlebarsUtil.registerDateHelpers(hb);
HandlebarsUtil.registerJurisdictionHelpers(hb, SimpleJurisdictionResolver.instance);
HandlebarsUtil.registerJavaScriptHelper(hb, StandardJsEngine::new);
return hb;
}

protected final Map<String, Class> storeTypes = new HashMap<>();
protected final Map<String, String> namedSessions = new HashMap<>();
public void addNamedSession (String name, String token) { namedSessions.put(name, token); }

public void reset () {
ctx.clear();
storeTypes.clear();
api.logout();
}

public ApiScript[] include (ApiScript script) {
final String includePath = handlebars(script.getInclude(), getContext());
if (getIncludeHandler() != null) {
final Map<String, Object> context = new HashMap<>(System.getenv());
context.putAll(getContext());
if (listener != null) listener.beforeCall(script, context);
return jsonWithComments(handlebars(getIncludeHandler().include(includePath),
HandlebarsUtil.apply(getHandlebars(), script.getParams(), context),
script.getParamStartDelim(), script.getParamEndDelim()), ApiScript[].class);
}
return notSupported("include("+ includePath +"): no includeHandler set");
}

public void run(String script) throws Exception { run(jsonWithComments(script, ApiScript[].class)); }

public boolean run(ApiScript[] scripts) throws Exception {
boolean allSucceeded = true;
for (ApiScript script : scripts) if (!run(script)) allSucceeded = false;
return allSucceeded;
}

public boolean run(ApiScript script) throws Exception {
if (listener != null) listener.setCtxVars(ctx);
currentApi = script.getConnection(this.api, currentApi, alternateApis, getHandlebars(), ctx);
if (script.shouldSkip(js, ctx)) return true;
if (script.hasInclude()) {
if (script.isIncludeDefaults()) return true; // skip this block. used in validation before running included script
final String logPrefix = (script.hasComment() ? script.getComment()+"\n" : "") + ">>> ";
log.info(logPrefix+"including script: '"+script.getInclude()+"'"+(script.hasParams()?" {"+ StringUtil.toString(NameAndValue.map2list(script.getParams()), ", ")+"}":""));
ApiScript[] include = include(script);
boolean paramsChanged = false;
if (include.length > 0 && include[0].isIncludeDefaults()) {
final ApiScript defaults = include[0];
if (empty(defaults.getParams())) {
log.warn(logPrefix+"no default parameters set");
} else {
for (Map.Entry<String, Object> param : defaults.getParams().entrySet()) {
final String pName = param.getKey();
final Object pValue = param.getValue();
if (empty(pName)) return die(logPrefix+"empty default param name");
if (pValue != null && (!script.hasParams() || !script.getParams().containsKey(pName) || empty(script.getParams().get(pName)))) {
if ((pValue instanceof String) && pValue.equals(PARAM_REQUIRED)) {
return die(logPrefix+"required parameter is undefined: "+pName);
}
if ((pValue instanceof Boolean) && !((Boolean) pValue)) {
continue; // boolean values already default to false, no need to change script
}
log.info(logPrefix+"parameter '"+pName+"' undefined, using default value ("+pValue+")");
script.setParam(pName, pValue);
paramsChanged = true;
}
}
}
}
if (paramsChanged) include = include(script); // re-include because params have changed

if (script.hasBefore() && listener != null) listener.beforeCall(script, getContext());
final boolean ok = run(include);
if (ok && script.hasAfter() && listener != null) listener.afterCall(script, getContext(), null);

log.info(">>> included script completed: '"+script.getInclude()+"'"+(script.hasParams()?" {"+ StringUtil.toString(NameAndValue.map2list(script.getParams()), ", ")+"}":"")+", ok="+ok);
return ok;

} else {
setScriptForThread(script);
if (script.hasDelay()) sleep(script.getDelayMillis(), "delaying before starting script: " + script);
if (listener != null) listener.beforeScript(script.getBefore(), getContext());
try {
script.setStart(now());
do {
if (runOnce(script)) return true;
sleep(Math.min(script.getTimeoutMillis() / 10, 1000), "waiting to retry script: " + script);
} while (!script.isTimedOut());
if (listener != null) listener.scriptTimedOut(script);
return false;

} catch (Exception e) {
log.warn("run(" + script + "): " + e, e);
throw e;

} finally {
if (listener != null) listener.afterScript(script.getAfter(), getContext());
}
}
}

public boolean runOnce(ApiScript script) throws Exception {
if (script.hasNested()) return runInner(script);

final ApiScriptRequest request = script.getRequest();
final String method = request.getMethod().toUpperCase();
ctx.put("now", script.getStart());

String oldSessionsId = null;
final ApiClientBase api = currentApi;

if (request.hasSession()) {
if (request.getSession().equals(NEW_SESSION)) {
api.logout();
} else if (request.getSession().equals(TEMPORAL_NEW_SESSION)) {
oldSessionsId = api.getToken();
api.logout();
} else {
final String sessionId = namedSessions.get(request.getSession());
if (sessionId == null) return die("Session named " + request.getSession() + " is not defined (" + namedSessions + ")");
api.setToken(sessionId);
}
}

if (request.hasHeaders()) api.setHeaders(request.getHeaders());
boolean isCaptureHeaders = api.isCaptureHeaders();
try {
if (script.hasResponse() && script.getResponse().isRaw()) api.setCaptureHeaders(true);

String uri = handlebars(request.getUri(), getContext());
if (!uri.startsWith("/")) uri = "/" + uri;
log.debug("runOnce: transformed uri from "+request.getUri()+" -> "+uri);

boolean success = true;
final RestResponse restResponse;

if (listener != null) listener.beforeCall(script, getContext());
switch (method) {
case HttpMethods.GET:
restResponse = api.doGet(uri);
break;

case HttpMethods.PUT:
restResponse = api.doPut(uri, subst(request));
api.removeHeaders();
break;

case HttpMethods.POST:
if (request.hasHeaders() && request.hasHeader(CONTENT_TYPE)) {
if (!request.getHeader(CONTENT_TYPE).equals(MULTIPART_FORM_DATA.getMimeType())) {
return die("run("+script+"): invalid request content type");
}

final String filePath = request.getEntity().get("file").textValue();
File file;
if (filePath.startsWith("data:")) {
file = FileUtil.temp(".tmp");
FileUtil.toFile(file, handlebars(filePath.substring("data:".length()), ctx));
} else {
if (empty(filePath)) die("run(" + script + "): file path doesn't exist");
file = new File(filePath);
if (!file.exists()) die("run(" + script + "): file doesn't exist");
}

restResponse = api.doPost(uri, file);
} else {
restResponse = api.doPost(uri, subst(request));
}
api.removeHeaders();
break;

case HttpMethods.DELETE:
restResponse = api.doDelete(uri);
break;

default:
return die("run("+script+"): invalid request method: "+method);
}
if (listener != null) listener.afterCall(script, getContext(), restResponse);

if (script.hasResponse()) {
final ApiScriptResponse response = script.getResponse();

if (!response.statusOk(restResponse.status)) {
if (listener != null) listener.statusCheckFailed(script, uri, restResponse);
}

final JsonNode responseEntity;
if (response.hasType() && response.getType().equals(String.class.getName()) && !(restResponse.json.startsWith("\"") && restResponse.json.endsWith("\""))) {
restResponse.json = "\"" + restResponse.json + "\"";
}
responseEntity = empty(restResponse.json) || response.isRaw() ? null : json(restResponse.json, JsonNode.class);
Object responseObject = responseEntity;

if (response.getStatus() == HttpStatusCodes.UNPROCESSABLE_ENTITY) {
if (responseEntity != null) {
responseObject = new ValidationErrors(
Arrays.asList(fromJsonOrDie(responseEntity, ConstraintViolationBean[].class)));
}
} else {
Class<?> storeClass = null;
if (response.hasType()) {
storeClass = forName(response.getType());
if (response.hasStore()) storeTypes.put(response.getStore(), storeClass);

} else if (response.hasStore()) {
storeClass = storeTypes.get(response.getStore());
}

// if HTTP header is telling us the type, try to use that
if (restResponse.hasHeader(api.getEntityTypeHeaderName())) {
String entityTypeHeaderValue = restResponse.header(api.getEntityTypeHeaderName());
storeClass = getResponseObjectClass(entityTypeHeaderValue, storeClass);
if (response.hasStore()) storeTypes.put(response.getStore(), storeClass);
}
if (response.isRaw()) {
if (response.hasStore()) {
storeTypes.put(response.getStore(), RestResponse.class);
ctx.put(response.getStore(), restResponse);
}
} else if (responseEntity != null) {
if (storeClass == null) {
if (responseEntity.isArray()) {
storeClass = Map[].class;
} else if (responseEntity.isObject()) {
storeClass = Map.class;
} else if (responseEntity.isTextual()) {
storeClass = String.class;
} else if (responseEntity.isIntegralNumber()) {
storeClass = Long.class;
} else if (responseEntity.isDouble()) {
storeClass = Double.class;
} else {
storeClass = JsonNode.class; // punt
}
}
try {
responseObject = fromJsonOrDie(responseEntity, storeClass);
} catch (IllegalStateException e) {
log.warn("runOnce: error parsing JSON: " + e);
responseObject = responseEntity;
}

if (response.hasStore()) ctx.put(response.getStore(), responseObject);

if (response.hasSession()) {
final JsonNode sessionIdNode;
if (response.getSession().equals(".")) {
sessionIdNode = responseEntity;
} else {
sessionIdNode = findNode(responseEntity, response.getSession());
}
if (sessionIdNode == null) {
if (listener != null) listener.sessionIdNotFound(script, restResponse);
} else {
final String sessionId = sessionIdNode.textValue();
if (empty(sessionId)) die("runOnce: empty sessionId: "+restResponse);
final String sessionName = response.hasSessionName() ? response.getSessionName() : DEFAULT_SESSION_NAME;
namedSessions.put(sessionName, sessionId);
api.setToken(sessionId);
}
}
}
}

ctx.put(CTX_RESPONSE, restResponse);

if (response.hasChecks()) {
if (response.hasDelay()) sleep(response.getDelayMillis(), "runOnce: delaying "+response.getDelay()+" before checking response conditions");

final Map<String, Object> localCtx = new HashMap<>(getContext());
localCtx.put(CTX_JSON, responseObject);

for (ApiScriptResponseCheck check : response.getCheck()) {
if (listener != null && listener.skipCheck(script, check)) continue;
final String condition = handlebars(check.getCondition(), localCtx);
Boolean result = null;
long timeout = check.getTimeoutMillis();
long checkStart = now();
do {
try {
result = js.evaluateBoolean(condition, localCtx);
if (result) break;
if (script.isTimedOut()) {
log.warn("runOnce("+script+"): condition check ("+condition+") returned false");
} else {
log.debug("runOnce("+script+"): condition check ("+condition+") returned false");
}
} catch (Exception e) {
if (script.isTimedOut()) {
log.warn("runOnce(" + script + "): condition check (" + condition + ") failed: " + e);
} else {
log.debug("runOnce(" + script + "): condition check (" + condition + ") failed: " + e);
}
}
sleep(Math.min(timeout/10, 1000), "waiting to retry condition: "+condition);
} while (now() - checkStart < timeout);

if (result == null || !result) {
success = false;
final String msg = result == null ? "Exception in execution" : "Failed condition";
if (listener != null) listener.conditionCheckFailed(msg, script, restResponse, check, localCtx);
}
}
}

} else if (restResponse.status != OK) {
success = false;
if (listener != null) listener.unexpectedResponse(script, restResponse);
}

if (listener != null) listener.scriptCompleted(script);

if (!empty(oldSessionsId)) api.setToken(oldSessionsId);

return success;
} finally {
api.setCaptureHeaders(isCaptureHeaders);
}
}

public static Class<?> getResponseObjectClass(String entityTypeHeaderValue, Class<?> currentClass) {
if (!empty(entityTypeHeaderValue) && !entityTypeHeaderValue.equals("[]")) {
if (entityTypeHeaderValue.contains("<") && entityTypeHeaderValue.endsWith(">")) {
entityTypeHeaderValue = entityTypeHeaderValue.substring(0, entityTypeHeaderValue.indexOf("<"));
}
try {
return forName(entityTypeHeaderValue);
} catch (Exception e) {
log.warn("runOnce: error instantiating type (will treat as JsonNode): " + entityTypeHeaderValue + ": " + e);
}
}
return currentClass;
}

private boolean runInner(ApiScript script) throws Exception {
final ApiInnerScript inner = script.getNested();
inner.setParent(script);
final List<ApiScript> scripts = inner.getAllScripts(js, getHandlebars(), getContext());
final Map<String, String> failedParams = new LinkedHashMap<>();
for (ApiScript s : scripts) {
try {
if (!run(s)) {
switch (inner.getRunMode()) {
case fail_fast: return false;
default: failedParams.put(json(s.getParams()), "(returned false)");
}
}
} catch (Exception e) {
switch (inner.getRunMode()) {
case fail_fast: throw e;
default: failedParams.put(json(s.getParams()), e.getClass().getSimpleName()+": "+e.getMessage());
}
}
}
if (!empty(failedParams)) {
switch (inner.getRunMode()) {
case run_all: return die("runInner: failed iterations:\n"+StringUtil.toString(failedParams.keySet(), "\n"));
default: return die("runInner: failed iterations:\n"+json(failedParams));
}
}
return true;
}

protected String subst(ApiScriptRequest request) {
String json = requestEntityJson(request);
json = TestNames.replaceTestNames(json);
json = trimQuotes(json);
return json;
}

protected String requestEntityJson(ApiScriptRequest request) {
final String json = request.getJsonEntity(getContext());
return request.isHandlebarsEnabled() ? handlebars(json, getContext()) : json;
}

protected String scriptName(ApiScript script, String name) { return "api-runner(" + script + "):" + name; }

protected String handlebars(String value, Map<String, Object> ctx) {
return HandlebarsUtil.apply(getHandlebars(), value, mergeEnv(ctx));
}

protected String handlebars(String value, Map<String, Object> ctx, char altStart, char altEnd) {
return HandlebarsUtil.apply(getHandlebars(), value, mergeEnv(ctx), altStart, altEnd);
}

private Map<String, Object> mergeEnv(Map<String, Object> ctx) {

final Map<String, String> env = System.getenv();
if (empty(ctx)) return empty(env) ? ctx : new HashMap<>(env);
if (empty(env)) return ctx;

final Map<String, Object> merged = new HashMap<>();
if (!empty(env)) merged.putAll(env);
if (!empty(ctx)) merged.putAll(ctx);

return merged;
}

}

+ 33
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerListener.java View File

@@ -0,0 +1,33 @@
package org.cobbzilla.wizard.client.script;

import org.cobbzilla.wizard.util.RestResponse;

import java.util.Map;

public interface ApiRunnerListener {

String getName();
ApiRunner getApiRunner();
void setApiRunner(ApiRunner runner);

default void setCtxVars(Map<String, Object> ctx) {}

void beforeScript(String before, Map<String, Object> ctx) throws Exception;
void afterScript(String after, Map<String, Object> ctx) throws Exception;

void beforeCall(ApiScript script, Map<String, Object> ctx);
void afterCall(ApiScript script, Map<String, Object> ctx, RestResponse response);

void statusCheckFailed(ApiScript script, String uri, RestResponse restResponse);
void conditionCheckFailed(String message, ApiScript script, RestResponse restResponse, ApiScriptResponseCheck check,
Map<String, Object> ctx);

void sessionIdNotFound(ApiScript script, RestResponse restResponse);

void scriptCompleted(ApiScript script);
void scriptTimedOut(ApiScript script);
void unexpectedResponse(ApiScript script, RestResponse restResponse);

boolean skipCheck(ApiScript script, ApiScriptResponseCheck check);

}

+ 67
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerListenerBase.java View File

@@ -0,0 +1,67 @@
package org.cobbzilla.wizard.client.script;

import lombok.Getter;
import lombok.Setter;
import org.cobbzilla.util.string.StringUtil;
import org.cobbzilla.wizard.util.RestResponse;

import java.util.Map;

import static java.lang.System.identityHashCode;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;

public class ApiRunnerListenerBase implements ApiRunnerListener {

public ApiRunnerListenerBase (String name) { setName(name); }

@Getter @Setter private String name;

@Getter private ApiRunner apiRunner;
@Override public void setApiRunner(ApiRunner runner) {
if (apiRunner != null) {
die("setApiRunner: runner already set: "+apiRunner);
}
apiRunner = runner;
}

public String getId () { return getName() + "/" + identityHashCode(this); }

@Override public void beforeCall(ApiScript script, Map<String, Object> ctx) {}
@Override public void afterCall(ApiScript script, Map<String, Object> ctx, RestResponse response) {}

@Override public void statusCheckFailed(ApiScript script, String uri, RestResponse restResponse) {
if (script.isTimedOut()) {
die("statusCheckFailed(" + getId() + "): request " + script.getRequestLine() + " (resulting with " + uri
+ ") expected " + script.getResponse().getStatus() + ", returned response " + restResponse.toString());
}
}

@Override public void conditionCheckFailed(String message, ApiScript script, RestResponse restResponse,
ApiScriptResponseCheck check, Map<String, Object> ctx) {
if (script.isTimedOut()) {
die("conditionCheckFailed(" + getId() + "): " + script.getRequestLine() + ":\n" +
message + "\n" +
"failed condition=" + check + "\n" +
"server response=" + restResponse + "\n" +
"ctx=" + StringUtil.toString(ctx));
}
}

@Override public void sessionIdNotFound(ApiScript script, RestResponse restResponse) {
if (script.isTimedOut()) die("sessionIdNotFound("+getId()+"): expected "+script.getResponse().getSession()+", server response="+restResponse);
}

@Override public void scriptCompleted(ApiScript script) {}

@Override public void scriptTimedOut(ApiScript script) { die("scriptTimedOut: script="+script+", timed out"); }

@Override public void unexpectedResponse(ApiScript script, RestResponse restResponse) {
if (script.isTimedOut()) die("unexpectedResponse("+getId()+"): script="+script+", server response="+restResponse);
}

@Override public void beforeScript(String before, Map<String, Object> ctx) throws Exception {}
@Override public void afterScript(String after, Map<String, Object> ctx) throws Exception {}

@Override public boolean skipCheck(ApiScript script, ApiScriptResponseCheck check) { return false; }

}

+ 71
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerListenerStreamLogger.java View File

@@ -0,0 +1,71 @@
package org.cobbzilla.wizard.client.script;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.wizard.util.RestResponse;

import java.io.Writer;
import java.util.Map;

@Slf4j @AllArgsConstructor
public class ApiRunnerListenerStreamLogger implements ApiRunnerListener {

@Getter private final String name;
@Getter private final Writer out;
@Getter @Setter private ApiRunner apiRunner;

public ApiRunnerListenerStreamLogger(String name, Writer out) {
this.name = name;
this.out = out;
}

@Override public void beforeScript(String before, Map<String, Object> ctx) throws Exception {
write("beforeScript / "+before);
}

@Override public void afterScript(String after, Map<String, Object> ctx) throws Exception {
write("afterScript / " + after);
}

private void write(String s) { try { out.write(s); } catch (Exception e) { log.error("write: "+e, e); } }

private String scriptInfo(ApiScript script) {
return script.getRequestLine() + (script.hasComment() ? " / " + script.getComment() : "");
}

@Override public void beforeCall(ApiScript script, Map<String, Object> ctx) {
write("beforeCall / " + scriptInfo(script));
}

@Override public void afterCall(ApiScript script, Map<String, Object> ctx, RestResponse response) {
write("afterCall / " + scriptInfo(script));
}

@Override public void statusCheckFailed(ApiScript script, String uri, RestResponse restResponse) {
write("statusCheckFailed / " + scriptInfo(script) + " / " + uri + " / " + restResponse);
}

@Override public void conditionCheckFailed(String message, ApiScript script, RestResponse restResponse, ApiScriptResponseCheck check, Map<String, Object> ctx) {
write("conditionCheckFailed / " + scriptInfo(script) + " / " + message + " / " + restResponse + " / " + check);
}

@Override public void sessionIdNotFound(ApiScript script, RestResponse restResponse) {
write("sessionIdNotFound / " + scriptInfo(script) + " / " + restResponse);
}

@Override public void scriptCompleted(ApiScript script) { write("scriptCompleted / " + scriptInfo(script)); }

@Override public void scriptTimedOut(ApiScript script) {
write("scriptTimedOut / " + scriptInfo(script));
}

@Override public void unexpectedResponse(ApiScript script, RestResponse restResponse) {
write("unexpectedResponse / " + scriptInfo(script) + " / " + restResponse);
}

@Override public boolean skipCheck(ApiScript script, ApiScriptResponseCheck check) {
return false;
}
}

+ 53
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerMultiListener.java View File

@@ -0,0 +1,53 @@
package org.cobbzilla.wizard.client.script;

import lombok.Getter;
import org.cobbzilla.wizard.util.RestResponse;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.reflect.ReflectionUtil.copy;

public class ApiRunnerMultiListener extends ApiRunnerListenerBase {

@Getter private List<ApiRunnerListener> apiListeners = new ArrayList<>();

public ApiRunnerMultiListener (String name) { super(name); }

@SuppressWarnings("unused") // called from ApiRunner copy constructor via reflection (which is called from ApiScriptMultiDriver.run)
public ApiRunnerMultiListener (ApiRunnerMultiListener other) {
super(other.getName());
for (ApiRunnerListener listener : other.apiListeners) addApiListener(copy(listener));
}

public void apply (ListenerFunction f) {
for (ApiRunnerListener l : apiListeners) f.apply(l);
}

public ApiRunnerMultiListener addApiListener(ApiRunnerListener listener) { apiListeners.add(listener); return this; }

public static ApiRunnerMultiListener addApiListener(ApiRunnerListener multi, ApiRunnerListener add) {
if (multi == null) return die("addApiListener: multi was null");
if (multi instanceof ApiRunnerMultiListener) return ((ApiRunnerMultiListener) multi).addApiListener(add);
return die("addApiListener: expected ApiRunnerMultiListener, but was "+multi.getClass().getName());
}

@Override public void beforeCall(ApiScript script, Map<String, Object> ctx) {
for (ApiRunnerListener sub : apiListeners) sub.beforeCall(script, ctx);
}

@Override public void afterCall(ApiScript script, Map<String, Object> ctx, RestResponse response) {
for (ApiRunnerListener sub : apiListeners) sub.afterCall(script, ctx, response);
}

@Override public void beforeScript(String before, Map<String, Object> ctx) throws Exception {
for (ApiRunnerListener sub : apiListeners) sub.beforeScript(before, ctx);
}

@Override public void afterScript(String after, Map<String, Object> ctx) throws Exception {
for (ApiRunnerListener sub : apiListeners) sub.afterScript(after, ctx);
}

}

+ 26
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiRunnerWithEnv.java View File

@@ -0,0 +1,26 @@
package org.cobbzilla.wizard.client.script;

import org.cobbzilla.util.javascript.StandardJsEngine;
import org.cobbzilla.wizard.client.ApiClientBase;

import java.util.Map;

public class ApiRunnerWithEnv extends ApiRunner {

private Map<String, String> env;

public ApiRunnerWithEnv(ApiClientBase api,
StandardJsEngine js,
ApiRunnerListener listener,
ApiScriptIncludeHandler includeHandler,
Map<String, String> env) {
super(js, api, listener, includeHandler);
this.env = env;
}

@Override public Map<String, Object> getContext() {
final Map<String, Object> ctx = super.getContext();
ctx.putAll(env);
return ctx;
}
}

+ 170
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScript.java View File

@@ -0,0 +1,170 @@
package org.cobbzilla.wizard.client.script;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.github.jknack.handlebars.Handlebars;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.javascript.StandardJsEngine;
import org.cobbzilla.wizard.client.ApiClientBase;

import java.util.HashMap;
import java.util.Map;

import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate;
import static org.cobbzilla.util.string.StringUtil.ellipsis;
import static org.cobbzilla.util.time.TimeUtil.parseDuration;

@Slf4j @Accessors(chain=true)
public class ApiScript {

public static final String DEFAULT_SESSION_NAME = "default";

@Getter @Setter private String comment;
public boolean hasComment () { return !empty(comment); }

public static final String INCLUDE_DEFAULTS = "_defaults";
public static final String PARAM_REQUIRED = "_required";
@Getter @Setter private String include;
public boolean hasInclude () { return !empty(include); }
@JsonIgnore public boolean isIncludeDefaults () { return hasInclude() && getInclude().equals(INCLUDE_DEFAULTS); }

@Getter @Setter private Map<String, Object> params;
public boolean hasParams () { return !empty(params); }
public void setParam(String name, Object value) {
if (params == null) params = new HashMap<>();
params.put(name, value);
}
public void addParams(Map<String, Object> params) {
if (params == null) params = new HashMap<>();
this.params.putAll(params);
}

@Getter @Setter private char paramStartDelim = '<';
@Getter @Setter private char paramEndDelim = '>';

@Getter @Setter private String onlyIf;
public boolean hasOnlyIf () { return onlyIf != null; }

@Getter @Setter private String unless;
public boolean hasUnless () { return unless != null; }

@Getter @Setter private String delay;
public boolean hasDelay () { return !empty(delay); }
@JsonIgnore public long getDelayMillis () { return parseDuration(delay); }

@Getter @Setter private String before;
public boolean hasBefore () { return !empty(before); }

@Getter @Setter private String after;
public boolean hasAfter () { return !empty(after); }

@Getter @Setter private String timeout;
@JsonIgnore public long getTimeoutMillis () { return parseDuration(timeout); }

@Getter @Setter private NamedApiConnectionInfo connection;
public boolean hasConnection () { return connection != null;
}
@Getter @Setter private ApiScriptRequest request;

@Getter @Setter private ApiScriptResponse response;
public boolean hasResponse () { return response != null; }
@JsonIgnore public String getRequestLine () { return request.getMethod() + " " + request.getUri(); }

@Getter @Setter private long start = now();
@JsonIgnore public long getAge () { return now() - start; }

@Getter @Setter private ApiInnerScript nested;
public boolean hasNested() { return nested != null && nested.hasScripts(); }

@JsonIgnore public boolean isTimedOut() { return getAge() > getTimeoutMillis(); }

@Override public String toString() {
return "{\n" +
" \"comment\": \"" + comment + "\"," +
" \"request\": \"" + ellipsis(json(request), 300) + "\",\n" +
" \"response\": \"" + ellipsis(json(response), 300) + "\",\n" +
"}";
}

public ApiClientBase getConnection(ApiClientBase defaultApi,
ApiClientBase currentApi,
Map<String, ApiClientBase> alternateApis,
Handlebars handlebars,
Map<String, Object> ctx) {

if (!hasConnection()) return currentApi != null ? currentApi : defaultApi;
final NamedApiConnectionInfo conn = HandlebarsUtil.applyReflectively(handlebars, getConnection(), ctx);
ApiClientBase alternate;

if (conn.hasBaseUri()) {
if (conn.hasName()) {
alternate = alternateApis.get(conn.getName());
if (alternate != null && !alternate.getConnectionInfo().equals(conn)) {
log.warn("getConnection: redefining connection: "+ conn.getName());
}
try {
alternate = instantiate(defaultApi.getClass(), conn);
} catch (Exception e) {
try {
alternate = instantiate(defaultApi.getClass());
alternate.setConnectionInfo(conn);
} catch (Exception e2) {
return die("getConnection: error instantiating new connection of type "+defaultApi.getClass()+": "+e);
}
}
alternateApis.put(conn.getName(), alternate);
return alternate;

} else {
log.warn("getConnection: using anonymous connection: "+ conn);
return new ApiClientBase(conn);
}

} else {
if (!conn.hasName()) return die("getConnection: neither name nor baseUri was provided");
if (conn.getName().equals(DEFAULT_SESSION_NAME)) {
return defaultApi;
}
alternate = alternateApis.get(conn.getName());

if (alternate == null) return die("getConnection: connection named '"+conn.getName()+"' not found");
return alternate;
}
}

public boolean shouldSkip(StandardJsEngine js, Map<String, Object> ctx) {
if (hasOnlyIf()) {
try {
if (js.evaluateBoolean(getOnlyIf(), ctx, false)) {
log.info("onlyIf '"+getOnlyIf()+"' returned true, NOT skipping script");
return false;
} else {
log.info("onlyIf '"+getOnlyIf()+"' returned false, skipping script");
return true;
}
} catch (Exception e) {
return die("runOnce: error evaluating onlyIf '"+getOnlyIf()+"': "+e);
}
}
if (hasUnless()) {
try {
if (js.evaluateBoolean(getUnless(), ctx, false)) {
log.info("unless '"+getUnless()+"' returned true, skipping script");
return true;
} else {
log.info("unless '"+getUnless()+"' returned false, NOT skipping script");
return false;
}
} catch (Exception e) {
return die("runOnce: error evaluating unless '"+getUnless()+"': "+e);
}
}
return false;
}
}

+ 27
- 0
wizard-client/src/main/java/org/cobbzilla/wizard/client/script/ApiScriptIncludeClasspathHandler.java View File

@@ -0,0 +1,27 @@
package org.cobbzilla.wizard.client.script;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

import java.nio.file.Paths;

import static org.cobbzilla.util.io.StreamUtil.stream2string;

@Accessors(chain=true)
public class ApiScriptIncludeClasspathHandler implements ApiScriptIncludeHandler {

@Getter @Setter private String includePrefix;
@Getter @Setter private String commonPath;