Browse Source

Merge branch 'trunk' into branch-feature-AMBARI-14714 and fixed some ambari-admin-view issues (mradhakrishnan)

Madhuvanthi Radhakrishnan 8 years ago
parent
commit
1efc4f0c70
100 changed files with 2707 additions and 684 deletions
  1. 1 1
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js
  2. 1 1
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsEditCtrl.js
  3. 2 1
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Stack.js
  4. 72 0
      ambari-admin/src/main/resources/ui/admin-web/app/styles/toggle-switch.css
  5. 2 1
      ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/modals/create.html
  6. 4 2
      ambari-admin/src/main/resources/ui/admin-web/app/views/authentication/main.html
  7. 4 2
      ambari-admin/src/main/resources/ui/admin-web/app/views/loginActivities/homeDirectory.html
  8. 4 2
      ambari-admin/src/main/resources/ui/admin-web/app/views/loginActivities/loginMessage.html
  9. 2 1
      ambari-admin/src/main/resources/ui/admin-web/app/views/userManagement/modals/groupCreate.html
  10. 8 3
      ambari-admin/src/main/resources/ui/admin-web/app/views/userManagement/modals/userCreate.html
  11. 8 4
      ambari-admin/src/main/resources/ui/admin-web/app/views/userManagement/userEdit.html
  12. 24 21
      ambari-admin/src/main/resources/ui/admin-web/bower.json
  13. 2 2
      ambari-admin/src/main/resources/ui/admin-web/package.json
  14. 5 4
      ambari-common/src/main/python/resource_management/libraries/functions/component_version.py
  15. 1 0
      ambari-common/src/main/python/resource_management/libraries/functions/constants.py
  16. 8 2
      ambari-common/src/main/python/resource_management/libraries/functions/copy_tarball.py
  17. 11 0
      ambari-infra/ambari-infra-manager/pom.xml
  18. 46 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/CompositeFileAction.java
  19. 54 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/Document.java
  20. 23 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentDestination.java
  21. 118 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportConfiguration.java
  22. 35 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportJobListener.java
  23. 112 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportProperties.java
  24. 47 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportStepListener.java
  25. 99 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExporter.java
  26. 135 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentItemReader.java
  27. 25 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentItemWriter.java
  28. 25 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentIterator.java
  29. 24 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentSource.java
  30. 25 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/FileAction.java
  31. 72 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/LocalDocumentItemWriter.java
  32. 64 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/S3Properties.java
  33. 51 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/S3Uploader.java
  34. 90 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrDocumentIterator.java
  35. 68 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrDocumentSource.java
  36. 115 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrQueryBuilder.java
  37. 69 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrQueryProperties.java
  38. 50 0
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/TarGzCompressor.java
  39. 13 8
      ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/manager/JobManager.java
  40. 12 0
      ambari-infra/ambari-infra-manager/src/main/resources/infra-manager.properties
  41. 1 1
      ambari-infra/ambari-infra-manager/src/main/resources/log4j2.xml
  42. 147 0
      ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/DocumentExporterTest.java
  43. 197 0
      ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/DocumentItemReaderTest.java
  44. 98 0
      ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/LocalDocumentItemWriterTest.java
  45. 113 0
      ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/SolrQueryBuilderTest.java
  46. 2 2
      ambari-logsearch/ambari-logsearch-logfeeder/src/main/java/org/apache/ambari/logfeeder/filter/FilterKeyValue.java
  47. 5 10
      ambari-logsearch/ambari-logsearch-server/src/main/java/org/apache/ambari/logsearch/web/filters/LogsearchAuthenticationEntryPoint.java
  48. 20 2
      ambari-logsearch/ambari-logsearch-web/src/app/classes/filtering.ts
  49. 2 1
      ambari-logsearch/ambari-logsearch-web/src/app/classes/models/app-state.ts
  50. 2 1
      ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/audit-logs-query-params.ts
  51. 2 1
      ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/service-logs-truncated-query-params.ts
  52. 25 0
      ambari-logsearch/ambari-logsearch-web/src/app/classes/string.ts
  53. 0 3
      ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less
  54. 0 4
      ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts
  55. 5 3
      ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.html
  56. 2 2
      ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.html
  57. 8 9
      ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.less
  58. 3 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts
  59. 8 10
      ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html
  60. 8 3
      ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.less
  61. 1 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.spec.ts
  62. 70 50
      ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts
  63. 1 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
  64. 3 2
      ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
  65. 2 11
      ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less
  66. 18 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
  67. 2 2
      ambari-logsearch/ambari-logsearch-web/src/app/components/pagination/pagination.component.html
  68. 16 8
      ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.html
  69. 14 13
      ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.less
  70. 113 39
      ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts
  71. 4 3
      ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html
  72. 0 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less
  73. 6 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.html
  74. 3 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.less
  75. 63 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts
  76. 24 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts
  77. 4 3
      ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less
  78. 3 1
      ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.spec.ts
  79. 1 1
      ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.ts
  80. 3 1
      ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts
  81. 3 1
      ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts
  82. 58 22
      ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
  83. 4 0
      ambari-logsearch/ambari-logsearch-web/src/app/services/utils.service.ts
  84. 1 1
      ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
  85. 10 10
      ambari-logsearch/ambari-logsearch-web/webpack.config.js
  86. 1 1
      ambari-logsearch/docker/test-config/logfeeder/logfeeder.properties
  87. 2 3
      ambari-metrics/ambari-metrics-hadoop-sink/pom.xml
  88. 5 0
      ambari-server/src/main/java/org/apache/ambari/server/actionmanager/ExecutionCommandWrapper.java
  89. 7 1
      ambari-server/src/main/java/org/apache/ambari/server/api/handlers/CreateHandler.java
  90. 7 1
      ambari-server/src/main/java/org/apache/ambari/server/api/handlers/UpdateHandler.java
  91. 3 1
      ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java
  92. 15 0
      ambari-server/src/main/java/org/apache/ambari/server/api/services/RootServiceComponentConfigurationService.java
  93. 0 46
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/AmbariConfiguration.java
  94. 0 43
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapConfigOperation.java
  95. 0 49
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapConfigurationRequest.java
  96. 0 185
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapConfigurationService.java
  97. 0 61
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapRequestInfo.java
  98. 33 5
      ambari-server/src/main/java/org/apache/ambari/server/api/services/serializers/JsonSerializer.java
  99. 19 0
      ambari-server/src/main/java/org/apache/ambari/server/api/services/stackadvisor/StackAdvisorRequest.java
  100. 9 7
      ambari-server/src/main/java/org/apache/ambari/server/api/services/stackadvisor/commands/StackAdvisorCommand.java

+ 1 - 1
ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js

@@ -63,7 +63,7 @@ angular.module('ambariAdminConsole', [
     };
   }]);
 
-  $httpProvider.responseInterceptors.push(['$rootScope', '$q', function (scope, $q) {
+  $httpProvider.interceptors.push(['$rootScope', '$q', function (scope, $q) {
     function success(response) {
       return response;
     }

+ 1 - 1
ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsEditCtrl.js

@@ -36,7 +36,7 @@ angular.module('ambariAdminConsole')
   $scope.isGPLAccepted = false;
 
   $scope.isGPLRepo = function (repository) {
-    return repository.Repositories.tags.indexOf('GPL') >= 0;
+    return repository.Repositories.tags && repository.Repositories.tags.indexOf('GPL') >= 0;
   };
 
   $scope.showRepo = function (repository) {

+ 2 - 1
ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Stack.js

@@ -405,7 +405,8 @@ angular.module('ambariAdminConsole')
               $http.post(url + '/operating_systems/' + os.OperatingSystems.os_type + '/repositories/' + repo.Repositories.repo_id + '?validate_only=true',
                 {
                   "Repositories": {
-                    "base_url": repo.Repositories.base_url
+                    "base_url": repo.Repositories.base_url,
+                    "repo_name": repo.Repositories.repo_name
                   }
                 },
                 {

+ 72 - 0
ambari-admin/src/main/resources/ui/admin-web/app/styles/toggle-switch.css

@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+.ats-switch {
+  border-radius: 20px;
+  height: 40px;
+  min-width: 66px;
+  width: 66px;
+  position: relative;
+  overflow: hidden;
+  border-color: #EBECF1;
+  border-width: 2px;
+}
+
+.ats-switch .knob {
+  margin-left: 34px;
+  border-radius: 20px;
+  height: 32px;
+  width: 32px;
+  margin-top: 2px;
+}
+
+.ats-switch .switch-left,
+.ats-switch .switch-right {
+  position: absolute;
+  right: -40px;
+}
+
+.ats-switch .switch-animate {
+  height: 100%;
+}
+
+.ats-switch .switch-off {
+  background-color: white;
+}
+
+.ats-switch .switch-on {
+  background-color: #1EB475;
+  left: -10%;
+}
+
+.ats-switch .switch-on .knob {
+  background-color: white;
+}
+
+.ats-switch .switch-off .knob {
+  background-color: #999;
+}
+
+.switch-option-label {
+  padding-left: 5px;
+  font-size: 14px;
+}
+
+.switch-inline-label {
+  line-height: 40px;
+}

+ 2 - 1
ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/modals/create.html

@@ -17,7 +17,7 @@
 -->
 <form role="form" id="create-instance-form" name="form.instanceCreateForm" novalidate>
 <div class="modal-header">
-  <h1 class="modal-title">
+  <h1 class="modal-title col-sm-8">
     <span ng-if="!instanceClone">
       {{'views.create' | translate}}
     </span>
@@ -25,6 +25,7 @@
       {{'views.clone' | translate}}
     </span>
   </h1>
+  <a class="close" aria-hidden="true" ng-click="cancel()">&times;</a>
 </div>
 <div class="modal-body" ng-hide="isLoading">
 

+ 4 - 2
ambari-admin/src/main/resources/ui/admin-web/app/views/authentication/main.html

@@ -28,9 +28,11 @@
   <div class="form-horizontal">
     <div class="form-group col-sm-12">{{'authentication.description' | translate}}</div>
     <div class="form-group">
-      <label class="control-label col-sm-4">{{'authentication.ldap' | translate}}</label>
+      <label class="switch-inline-label col-sm-4">{{'authentication.ldap' | translate}}</label>
       <span class="col-sm-8">
-        <toggle-switch model="isLDAPEnabled" ng-disabled="isRequestRunning" on-label="{{'authentication.on' | translate}}" off-label="{{'authentication.off' | translate}}" class="switch-primary" data-off-color="danger" on-change="toggleAuthentication()"></toggle-switch>
+        <toggle-switch model="isLDAPEnabled" ng-disabled="isRequestRunning" class="switch-success" data-off-color="danger" on-change="toggleAuthentication()"></toggle-switch>
+        <span ng-if="isLDAPEnabled" class="switch-option-label">{{'authentication.on' | translate}}</span>
+        <span ng-if="!isLDAPEnabled" class="switch-option-label">{{'authentication.off' | translate}}</span>
       </span>
     </div>
   </div>

+ 4 - 2
ambari-admin/src/main/resources/ui/admin-web/app/views/loginActivities/homeDirectory.html

@@ -25,9 +25,11 @@
       </div>
       <fieldset>
         <div class="form-group">
-          <label class="col-sm-4 control-label">{{'common.loginActivities.homeDirectory.autoCreate' | translate}}</label>
+          <label class="col-sm-4 switch-inline-label">{{'common.loginActivities.homeDirectory.autoCreate' | translate}}</label>
           <div class="col-sm-8">
-            <toggle-switch model="autoCreate" on-label="{{'common.enabled' | translate}}" off-label="{{'common.disabled' | translate}}" class="switch-primary"></toggle-switch>
+            <toggle-switch model="autoCreate" class="switch-success"></toggle-switch>
+            <span ng-if="autoCreate" class="switch-option-label">{{'common.enabled' | translate}}</span>
+            <span ng-if="!autoCreate" class="switch-option-label">{{'common.disabled' | translate}}</span>
           </div>
           <input type="checkbox" name="autoCreate" class="hidden" ng-model="autoCreate">
         </div>

+ 4 - 2
ambari-admin/src/main/resources/ui/admin-web/app/views/loginActivities/loginMessage.html

@@ -22,9 +22,11 @@
     <div class="well">
       <fieldset>
         <div class="form-group">
-          <label class="col-sm-2 control-label">{{'common.loginActivities.status' | translate}}</label>
+          <label class="col-sm-2 switch-inline-label">{{'common.loginActivities.status' | translate}}</label>
           <div class="col-sm-10">
-            <toggle-switch ng-click="changeStatus();" model="status" on-label="{{'common.enabled' | translate}}" off-label="{{'common.disabled' | translate}}" class="switch-primary userstatus" data-off-color="disabled"></toggle-switch>
+            <toggle-switch ng-click="changeStatus();" model="status" class="switch-success" data-off-color="disabled"></toggle-switch>
+            <span ng-if="status" class="switch-option-label">{{'common.enabled' | translate}}</span>
+            <span ng-if="!status" class="switch-option-label">{{'common.disabled' | translate}}</span>
           </div>
           <input type="checkbox" name="status" class="hidden" ng-model="status">
         </div>

+ 2 - 1
ambari-admin/src/main/resources/ui/admin-web/app/views/userManagement/modals/groupCreate.html

@@ -18,9 +18,10 @@
 
 <form id="create-group-form" role="form" novalidate name="form.groupCreateForm">
   <div class="modal-header">
-    <h1 class="modal-title">
+    <h1 class="modal-title col-sm-8">
       {{'groups.createLocal' | translate}}
     </h1>
+    <a class="close" aria-hidden="true" ng-click="cancel()">&times;</a>
   </div>
   <div class="modal-body">
     <div class="form-group"

+ 8 - 3
ambari-admin/src/main/resources/ui/admin-web/app/views/userManagement/modals/userCreate.html

@@ -18,9 +18,10 @@
 
 <form id="create-user-form" role="form" novalidate name="form.userCreateForm">
   <div class="modal-header">
-    <h1 class="modal-title">
+    <h1 class="modal-title col-sm-8">
       {{'users.create' | translate}}
     </h1>
+    <a class="close" aria-hidden="true" ng-click="cancel()">&times;</a>
   </div>
   <div class="modal-body">
     <div class="form-group"
@@ -125,7 +126,9 @@
         <i class="fa fa-question-circle" aria-hidden="true"></i>
       </label>
       <div>
-        <toggle-switch model="formData.isAdmin" on-label="{{'common.yes' | translate}}" off-label="{{'common.no' | translate}}" class="switch-primary" data-off-color="danger"></toggle-switch>
+        <toggle-switch model="formData.isAdmin" class="switch-success" data-off-color="danger"></toggle-switch>
+        <span ng-if="formData.isAdmin" class="switch-option-label">{{'common.yes' | translate}}</span>
+        <span ng-if="!formData.isAdmin" class="switch-option-label">{{'common.no' | translate}}</span>
       </div>
     </div>
 
@@ -135,7 +138,9 @@
         <i class="fa fa-question-circle" aria-hidden="true"></i>
       </label>
       <div>
-        <toggle-switch model="formData.isActive" on-label="{{'users.active' | translate}}" off-label="{{'users.inactive' | translate}}" class="switch-primary" data-off-color="danger"></toggle-switch>
+        <toggle-switch model="formData.isActive" class="switch-success" data-off-color="danger"></toggle-switch>
+        <span ng-if="formData.isActive" class="switch-option-label">{{'users.active' | translate}}</span>
+        <span ng-if="!formData.isActive" class="switch-option-label">{{'users.inactive' | translate}}</span>
       </div>
     </div>
 

+ 8 - 4
ambari-admin/src/main/resources/ui/admin-web/app/views/userManagement/userEdit.html

@@ -38,15 +38,19 @@
       </div>
     </div>
     <div class="form-group">
-      <label class="col-sm-2 ">{{'users.status' | translate}}</label>
+      <label class="col-sm-2 switch-inline-label">{{'users.status' | translate}}</label>
       <div class="col-sm-10">
-        <toggle-switch on-change="toggleUserActive()" disabled-tooltip="{{'users.alerts.cannotChange' | translate: '{term: constants.status}'}}" ng-disabled="isCurrentUser" model="user.active" on-label="{{'users.active' | translate}}" off-label="{{'users.inactive' | translate}}" class="switch-primary userstatus {{user ? '' : 'no-animation'}}" data-off-color="danger"></toggle-switch>
+        <toggle-switch on-change="toggleUserActive()" disabled-tooltip="{{'users.alerts.cannotChange' | translate: '{term: constants.status}'}}" ng-disabled="isCurrentUser" model="user.active" class="switch-success userstatus {{user ? '' : 'no-animation'}}" data-off-color="danger"></toggle-switch>
+        <span ng-if="user.active" class="switch-option-label">{{'users.active' | translate}}</span>
+        <span ng-if="!user.active" class="switch-option-label">{{'users.inactive' | translate}}</span>
       </div>
     </div>
     <div class="form-group">
-      <label class="col-sm-2 "><span class="glyphicon glyphicon-flash"></span> {{'users.ambariAdmin' | translate}}</label>
+      <label class="col-sm-2 switch-inline-label"><span class="glyphicon glyphicon-flash"></span> {{'users.ambariAdmin' | translate}}</label>
       <div class="col-sm-10">
-        <toggle-switch on-change="toggleUserAdmin()" disabled-tooltip="{{'users.alerts.cannotChange' | translate: '{term: constants.admin}'}}" ng-disabled="isCurrentUser" model="user.admin" on-label="{{'common.yes' | translate}}" off-label="{{'common.no' | translate}}" class="switch-primary userstatus {{user ? '' : 'no-animation'}}" data-off-color="danger"></toggle-switch>
+        <toggle-switch on-change="toggleUserAdmin()" disabled-tooltip="{{'users.alerts.cannotChange' | translate: '{term: constants.admin}'}}" ng-disabled="isCurrentUser" model="user.admin" class="switch-success userstatus {{user ? '' : 'no-animation'}}" data-off-color="danger"></toggle-switch>
+        <span ng-if="user.admin" class="switch-option-label">{{'common.yes' | translate}}</span>
+        <span ng-if="!user.admin" class="switch-option-label">{{'common.no' | translate}}</span>
       </div>
     </div>
     <div class="form-group">

+ 24 - 21
ambari-admin/src/main/resources/ui/admin-web/bower.json

@@ -1,23 +1,26 @@
 {
-  "name": "adminconsole",
-  "private": true,
-  "dependencies": {
-    "bootstrap": "3.3.7",
-    "angular": "1.2.26",
-    "angular-route": "1.2.26",
-    "angular-bootstrap": "0.11.0",
-    "underscore": "1.7.0",
-    "restangular": "1.4.0",
-    "angular-bootstrap-toggle-switch": "0.5.1",
-    "angular-animate": "1.2.26",
-    "angular-translate": "2.2.0",
-    "font-awesome": "4.2.0"
-  },
-  "devDependencies": {
-    "angular-mocks": "1.2.26",
-    "commonjs": "0.2.0",
-    "chai": "1.8.0",
-    "mocha": "1.14.0",
-    "sinon": "1.10.3"
-  }
+	"name": "adminconsole",
+	"private": true,
+	"dependencies": {
+		"bootstrap": "3.1.1",
+		"angular": "1.5.11",
+		"angular-route": "1.5.11",
+		"angular-bootstrap": "0.11.0",
+		"underscore": "1.7.0",
+		"restangular": "1.4.0",
+		"angular-bootstrap-toggle-switch": "0.5.1",
+		"angular-animate": "1.5.11",
+		"angular-translate": "2.2.0",
+		"font-awesome": "4.2.0"
+	},
+	"devDependencies": {
+		"angular-mocks": "1.5.11",
+		"commonjs": "0.2.0",
+		"chai": "1.8.0",
+		"mocha": "1.14.0",
+		"sinon": "1.10.3"
+	},
+	"resolutions": {
+		"angular": "1.5.11"
+	}
 }

+ 2 - 2
ambari-admin/src/main/resources/ui/admin-web/package.json

@@ -3,7 +3,7 @@
   "version": "0.0.0",
   "dependencies": {},
   "devDependencies": {
-    "bower": "1.3.8",
+    "bower": "^1.3.8",
     "gulp": "^3.8.8",
     "gulp-add-src": "^0.2.0",
     "gulp-autoprefixer": "0.0.7",
@@ -14,7 +14,7 @@
     "gulp-flatten": "0.0.2",
     "gulp-load-plugins": "0.5.0",
     "gulp-order": "1.1.1",
-    "gulp-plumber": "0.6.6",
+    "gulp-plumber": "1.1.0",
     "gulp-size": "0.3.0",
     "gulp-uglify": "0.2.1",
     "gulp-useref": "0.4.2",

+ 5 - 4
ambari-common/src/main/python/resource_management/libraries/functions/component_version.py

@@ -20,7 +20,7 @@ limitations under the License.
 
 from resource_management.libraries.script.script import Script
 
-def get_component_repository_version(service_name = None, component_name = None):
+def get_component_repository_version(service_name = None, component_name = None, default_value = None):
   """
   Gets the version associated with the specified component from the structure in the command.
   Every command should contain a mapping of service/component to the desired repository it's set
@@ -28,22 +28,23 @@ def get_component_repository_version(service_name = None, component_name = None)
 
   :service_name: the name of the service
   :component_name: the name of the component
+  :default_value: the value to return if either the service or the component are not found
   """
   config = Script.get_config()
 
   versions = _get_component_repositories(config)
   if versions is None:
-    return None
+    return default_value
 
   if service_name is None:
     service_name = config['serviceName'] if config is not None and 'serviceName' in config else None
 
   if service_name is None or service_name not in versions:
-    return None
+    return default_value
 
   component_versions = versions[service_name]
   if len(component_versions) == 0:
-    return None
+    return default_value
 
   if component_name is None:
     component_name = config["role"] if config is not None and "role" in config else None

+ 1 - 0
ambari-common/src/main/python/resource_management/libraries/functions/constants.py

@@ -120,3 +120,4 @@ class StackFeature:
   RANGER_KMS_SSL = "ranger_kms_ssl"
   KAFKA_ACL_MIGRATION_SUPPORT = "kafka_acl_migration_support"
   ATLAS_CORE_SITE_SUPPORT="atlas_core_site_support"
+  KAFKA_EXTENDED_SASL_SUPPORT = "kafka_extended_sasl_support"

+ 8 - 2
ambari-common/src/main/python/resource_management/libraries/functions/copy_tarball.py

@@ -90,8 +90,14 @@ def _prepare_tez_tarball():
   # if enabled, LZO GPL libraries must be copied as well
   if lzo_utils.should_install_lzo():
     stack_root = Script.get_stack_root()
-    tez_version = component_version.get_component_repository_version("TEZ")
-    hadoop_lib_native_lzo_dir = os.path.join(stack_root, tez_version, "hadoop", "lib", "native")
+    service_version = component_version.get_component_repository_version(service_name = "TEZ")
+
+    # some installations might not have Tez, but MapReduce2 should be a fallback to get the LZO libraries from
+    if service_version is None:
+      Logger.warning("Tez does not appear to be installed, using the MapReduce version to get the LZO libraries")
+      service_version = component_version.get_component_repository_version(service_name = "MAPREDUCE2")
+
+    hadoop_lib_native_lzo_dir = os.path.join(stack_root, service_version, "hadoop", "lib", "native")
 
     if not sudo.path_isdir(hadoop_lib_native_lzo_dir):
       Logger.warning("Unable to located native LZO libraries at {0}, falling back to hadoop home".format(hadoop_lib_native_lzo_dir))

+ 11 - 0
ambari-infra/ambari-infra-manager/pom.xml

@@ -141,6 +141,12 @@
       <version>3.4</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-all</artifactId>
+      <version>1.3</version>
+      <scope>test</scope>
+    </dependency>
     <!-- Spring dependencies -->
     <dependency>
       <groupId>org.springframework</groupId>
@@ -417,6 +423,11 @@
       <groupId>com.google.guava</groupId>
       <version>20.0</version>
     </dependency>
+    <dependency>
+      <groupId>com.amazonaws</groupId>
+      <artifactId>aws-java-sdk-s3</artifactId>
+      <version>1.11.5</version>
+    </dependency>
   </dependencies>
 
 </project>

+ 46 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/CompositeFileAction.java

@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import java.io.File;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+public class CompositeFileAction implements FileAction {
+
+  private final List<FileAction> actions;
+
+  public CompositeFileAction(FileAction... actions) {
+    this.actions = asList(actions);
+  }
+
+  public void add(FileAction action) {
+    actions.add(action);
+  }
+
+  @Override
+  public File perform(File inputFile) {
+    File file = inputFile;
+    for (FileAction action : actions) {
+      file = action.perform(file);
+    }
+    return file;
+  }
+}

+ 54 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/Document.java

@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.util.Collections.unmodifiableMap;
+
+// TODO: create entities for each solr collections
+public class Document {
+  private final Map<String, String> fieldMap;
+
+  private Document() {
+    fieldMap = new HashMap<>();
+  }
+
+  public Document(Map<String, String> fieldMap) {
+    this.fieldMap = unmodifiableMap(fieldMap);
+  }
+
+  public String get(String key) {
+    return fieldMap.get(key);
+  }
+
+  @JsonAnyGetter
+  private Map<String, String> getFieldMap() {
+    return fieldMap;
+  }
+
+  @JsonAnySetter
+  private void put(String key, String value) {
+    fieldMap.put(key, value);
+  }
+}

+ 23 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentDestination.java

@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+public interface DocumentDestination {
+  DocumentItemWriter open(Document firstDocument);
+}

+ 118 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportConfiguration.java

@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.Step;
+import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.JobScope;
+import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.StepScope;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.nio.file.Paths;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+
+import static org.apache.ambari.infra.job.archive.SolrDocumentSource.SOLR_DATETIME_FORMATTER;
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+@Configuration
+public class DocumentExportConfiguration {
+  private static final Logger LOG = LoggerFactory.getLogger(DocumentExportConfiguration.class);
+  private static final DateTimeFormatter FILENAME_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH_mm_ss.SSSX");
+
+  @Inject
+  private DocumentExportProperties properties;
+
+  @Inject
+  private StepBuilderFactory steps;
+
+  @Inject
+  private JobBuilderFactory jobs;
+
+
+
+  @Bean
+  public Job logExportJob(@Qualifier("exportStep") Step logExportStep) {
+    return jobs.get("solr_data_export").listener(new DocumentExportJobListener()).start(logExportStep).build();
+  }
+
+  @Bean
+  @JobScope
+  public Step exportStep(DocumentExporter documentExporter) {
+    return steps.get("export")
+            .tasklet(documentExporter)
+            .listener(new DocumentExportStepListener(properties))
+            .build();
+  }
+
+  @Bean
+  @StepScope
+  public DocumentExporter getDocumentExporter(DocumentItemReader documentItemReader,
+                                              @Value("#{stepExecution.jobExecution.id}") String jobId) {
+    File path = Paths.get(
+            properties.getDestinationDirectoryPath(),
+            String.format("%s_%s", properties.getQuery().getCollection(), jobId)).toFile(); // TODO: add end date
+    LOG.info("Destination directory path={}", path);
+    if (!path.exists()) {
+      if (!path.mkdirs()) {
+        LOG.warn("Unable to create directory {}", path);
+      }
+    }
+
+    CompositeFileAction fileAction = new CompositeFileAction(new TarGzCompressor());
+
+    return new DocumentExporter(
+            documentItemReader,
+            firstDocument -> new LocalDocumentItemWriter(
+                    new File(path, String.format("%s_-_%s.json",
+                            properties.getQuery().getCollection(),
+                            firstDocument.get(properties.getFileNameSuffixColumn()))),
+                    fileAction),
+            properties.getWriteBlockSize());
+  }
+
+  @Bean
+  @StepScope
+  public DocumentItemReader reader(DocumentSource documentSource) {
+    return new DocumentItemReader(documentSource, properties.getReadBlockSize());
+  }
+
+  @Bean
+  @StepScope
+  public DocumentSource logSource(@Value("#{jobParameters[endDate]}") String endDateText) {
+    OffsetDateTime endDate = OffsetDateTime.now(ZoneOffset.UTC);
+    if (!isBlank(endDateText))
+      endDate = OffsetDateTime.parse(endDateText);
+
+    return new SolrDocumentSource(
+            properties.getZooKeeperSocket(),
+            properties.getQuery(),
+            SOLR_DATETIME_FORMATTER.format(endDate));
+  }
+}

+ 35 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportJobListener.java

@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobExecutionListener;
+
+public class DocumentExportJobListener implements JobExecutionListener {
+  @Override
+  public void beforeJob(JobExecution jobExecution) {
+
+  }
+
+  @Override
+  public void afterJob(JobExecution jobExecution) {
+    jobExecution.setExitStatus(new ExitStatus(ExitStatus.COMPLETED.getExitCode()));
+  }
+}

+ 112 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportProperties.java

@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.hibernate.validator.constraints.NotBlank;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+import javax.validation.constraints.Min;
+
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+@Configuration
+@PropertySource(value = {"classpath:infra-manager.properties"})
+@ConfigurationProperties(prefix = "infra-manager.jobs.solr_data_export")
+public class DocumentExportProperties {
+  @NotBlank
+  private String zooKeeperSocket;
+  @Min(1)
+  private int readBlockSize;
+  @Min(1)
+  private int writeBlockSize;
+  @NotBlank
+  private String destinationDirectoryPath;
+  @NotBlank
+  private String fileNameSuffixColumn;
+  private SolrQueryProperties query;
+
+  public String getZooKeeperSocket() {
+    return zooKeeperSocket;
+  }
+
+  public void setZooKeeperSocket(String zooKeeperSocket) {
+    this.zooKeeperSocket = zooKeeperSocket;
+  }
+
+  public int getReadBlockSize() {
+    return readBlockSize;
+  }
+
+  public void setReadBlockSize(int readBlockSize) {
+    this.readBlockSize = readBlockSize;
+  }
+
+  public int getWriteBlockSize() {
+    return writeBlockSize;
+  }
+
+  public void setWriteBlockSize(int writeBlockSize) {
+    this.writeBlockSize = writeBlockSize;
+  }
+
+  public String getDestinationDirectoryPath() {
+    return destinationDirectoryPath;
+  }
+
+  public void setDestinationDirectoryPath(String destinationDirectoryPath) {
+    this.destinationDirectoryPath = destinationDirectoryPath;
+  }
+
+  public void apply(JobParameters jobParameters) {
+    // TODO: solr query params
+    zooKeeperSocket = jobParameters.getString("zooKeeperSocket", zooKeeperSocket);
+    readBlockSize = getIntJobParameter(jobParameters, "readBlockSize", readBlockSize);
+    writeBlockSize = getIntJobParameter(jobParameters, "writeBlockSize", writeBlockSize);
+    destinationDirectoryPath = jobParameters.getString("destinationDirectoryPath", destinationDirectoryPath);
+    query.setCollection(jobParameters.getString("collection", query.getCollection()));
+    query.setQueryText(jobParameters.getString("queryText", query.getQueryText()));
+    query.setFilterQueryText(jobParameters.getString("filterQueryText", query.getFilterQueryText()));
+  }
+
+  private int getIntJobParameter(JobParameters jobParameters, String parameterName, int defaultValue) {
+    String writeBlockSizeText = jobParameters.getString(parameterName);
+    if (isBlank(writeBlockSizeText))
+      return defaultValue;
+    return this.writeBlockSize = Integer.parseInt(writeBlockSizeText);
+  }
+
+  public String getFileNameSuffixColumn() {
+    return fileNameSuffixColumn;
+  }
+
+  public void setFileNameSuffixColumn(String fileNameSuffixColumn) {
+    this.fileNameSuffixColumn = fileNameSuffixColumn;
+  }
+
+  public SolrQueryProperties getQuery() {
+    return query;
+  }
+
+  public void setQuery(SolrQueryProperties query) {
+    this.query = query;
+  }
+}

+ 47 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExportStepListener.java

@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.StepExecutionListener;
+
+public class DocumentExportStepListener implements StepExecutionListener {
+  private static final Logger LOG = LoggerFactory.getLogger(DocumentExportStepListener.class);
+
+  private final DocumentExportProperties properties;
+
+  public DocumentExportStepListener(DocumentExportProperties properties) {
+    this.properties = properties;
+  }
+
+  @Override
+  public void beforeStep(StepExecution stepExecution) {
+    properties.apply(stepExecution.getJobParameters());
+    LOG.info("LogExport step - before step execution");
+  }
+
+  @Override
+  public ExitStatus afterStep(StepExecution stepExecution) {
+    LOG.info("LogExport step - after step execution");
+    return stepExecution.getExitStatus();
+  }
+}

+ 99 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentExporter.java

@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.StepExecutionListener;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.core.step.tasklet.Tasklet;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamReader;
+import org.springframework.batch.repeat.RepeatStatus;
+
+public class DocumentExporter implements Tasklet, StepExecutionListener {
+
+  private boolean complete = false;
+  private final ItemStreamReader<Document> documentReader;
+  private final DocumentDestination documentDestination;
+  private final int writeBlockSize;
+
+  public DocumentExporter(ItemStreamReader<Document> documentReader, DocumentDestination documentDestination, int writeBlockSize) {
+    this.documentReader = documentReader;
+    this.documentDestination = documentDestination;
+    this.writeBlockSize = writeBlockSize;
+  }
+
+  @Override
+  public void beforeStep(StepExecution stepExecution) {
+
+  }
+
+  @Override
+  public ExitStatus afterStep(StepExecution stepExecution) {
+    if (complete) {
+      return ExitStatus.COMPLETED;
+    }
+		else {
+      return ExitStatus.FAILED;
+    }
+  }
+
+  @Override
+  public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
+    ExecutionContext executionContext = chunkContext.getStepContext().getStepExecution().getExecutionContext();
+    documentReader.open(executionContext);
+
+    DocumentItemWriter writer = null;
+    int writtenCount = 0;
+    try {
+      Document document;
+      while ((document = documentReader.read()) != null) {
+        if (writer != null && writtenCount >= writeBlockSize) {
+          writer.close();
+          writer = null;
+          writtenCount = 0;
+          documentReader.update(executionContext);
+        }
+
+        if (writer == null)
+          writer = documentDestination.open(document);
+
+        writer.write(document);
+        ++writtenCount;
+      }
+    }
+    catch (Exception e) {
+      if (writer != null) {
+        writer.revert();
+        writer = null;
+      }
+      throw e;
+    }
+    finally {
+      if (writer != null)
+        writer.close();
+      documentReader.close();
+    }
+
+    complete = true;
+    return RepeatStatus.FINISHED;
+  }
+}

+ 135 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentItemReader.java

@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamException;
+import org.springframework.batch.item.support.AbstractItemStreamItemReader;
+import org.springframework.batch.repeat.CompletionPolicy;
+import org.springframework.batch.repeat.RepeatContext;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.batch.repeat.context.RepeatContextSupport;
+import org.springframework.util.ClassUtils;
+
+public class DocumentItemReader extends AbstractItemStreamItemReader<Document> implements CompletionPolicy {
+
+  public final static String POSITION = "last-read";
+
+  private final DocumentSource documentSource;
+  private final int readBlockSize;
+
+  private DocumentIterator documentIterator = null;
+  private int count = 0;
+  private boolean eof = false;
+  private Document current = null;
+  private Document previous = null;
+
+  public DocumentItemReader(DocumentSource documentSource, int readBlockSize) {
+    this.documentSource = documentSource;
+    this.readBlockSize = readBlockSize;
+    setName(ClassUtils.getShortName(DocumentItemReader.class));
+  }
+
+  @Override
+  public Document read() throws Exception {
+    if (documentIterator == null)
+      openStream();
+    Document next = getNext();
+    if (next == null && count > readBlockSize) {
+      openStream();
+      next = getNext();
+    }
+    eof = next == null;
+    if (eof && documentIterator != null)
+      documentIterator.close();
+
+    previous = current;
+    current = next;
+    return current;
+  }
+
+  private Document getNext() {
+    ++count;
+    return documentIterator.next();
+  }
+
+  private void openStream() {
+    closeStream();
+    documentIterator = documentSource.open(current, readBlockSize);
+    count = 0;
+  }
+
+  private void closeStream() {
+    if (documentIterator == null)
+      return;
+    try {
+      documentIterator.close();
+    }
+    catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+    documentIterator = null;
+  }
+
+  @Override
+  public void open(ExecutionContext executionContext) {
+    super.open(executionContext);
+    current = null;
+    previous = null;
+    eof = false;
+    documentIterator = null;
+    if (!executionContext.containsKey(POSITION))
+      return;
+
+    current = (Document) executionContext.get(POSITION);
+  }
+
+  @Override
+  public void update(ExecutionContext executionContext) throws ItemStreamException {
+    super.update(executionContext);
+    if (previous != null)
+      executionContext.put(POSITION, previous);
+  }
+
+  @Override
+  public void close() {
+    closeStream();
+  }
+
+  @Override
+  public boolean isComplete(RepeatContext context, RepeatStatus result) {
+    return eof;
+  }
+
+  @Override
+  public boolean isComplete(RepeatContext context) {
+    return eof;
+  }
+
+  @Override
+  public RepeatContext start(RepeatContext parent) {
+    return new RepeatContextSupport(parent);
+  }
+
+  @Override
+  public void update(RepeatContext context) {
+    if (eof)
+      context.setCompleteOnly();
+  }
+}

+ 25 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentItemWriter.java

@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+public interface DocumentItemWriter {
+  void write(Document document);
+  void revert();
+  void close();
+}

+ 25 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentIterator.java

@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import java.util.Iterator;
+
+// TODO: generic closeable iterator
+public interface DocumentIterator extends Iterator<Document>, AutoCloseable {
+}

+ 24 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/DocumentSource.java

@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+// TODO: generic object source
+public interface DocumentSource {
+  DocumentIterator open(Document current, int rows);
+}

+ 25 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/FileAction.java

@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import java.io.File;
+
+public interface FileAction {
+  File perform(File inputFile);
+}

+ 72 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/LocalDocumentItemWriter.java

@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.IOUtils;
+
+import java.io.*;
+
+public class LocalDocumentItemWriter implements DocumentItemWriter {
+  private static final ObjectMapper json = new ObjectMapper();
+  private static final String ENCODING = "UTF-8";
+
+  private final File outFile;
+  private final BufferedWriter bufferedWriter;
+  private final FileAction fileAction;
+
+  public LocalDocumentItemWriter(File outFile, FileAction fileAction) {
+    this.fileAction = fileAction;
+    this.outFile = outFile;
+    try {
+      this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile), ENCODING));
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    } catch (FileNotFoundException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  @Override
+  public void write(Document document) {
+    try {
+      bufferedWriter.write(json.writeValueAsString(document));
+      bufferedWriter.newLine();
+    }
+    catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  @Override
+  public void revert() {
+    IOUtils.closeQuietly(bufferedWriter);
+    outFile.delete();
+  }
+
+  @Override
+  public void close() {
+    try {
+      bufferedWriter.close();
+      fileAction.perform(outFile);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+}

+ 64 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/S3Properties.java

@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+public class S3Properties {
+  @NotBlank
+  private String accessKey;
+  @NotBlank
+  private String secretKey;
+  @NotBlank
+  private String keyPrefix;
+  @NotBlank
+  private String bucketName;
+
+  public String getAccessKey() {
+    return accessKey;
+  }
+
+  public String getSecretKey() {
+    return secretKey;
+  }
+
+  public String getKeyPrefix() {
+    return keyPrefix;
+  }
+
+  public String getBucketName() {
+    return bucketName;
+  }
+
+  public void setAccessKey(String accessKey) {
+    this.accessKey = accessKey;
+  }
+
+  public void setSecretKey(String secretKey) {
+    this.secretKey = secretKey;
+  }
+
+  public void setKeyPrefix(String keyPrefix) {
+    this.keyPrefix = keyPrefix;
+  }
+
+  public void setBucketName(String bucketName) {
+    this.bucketName = bucketName;
+  }
+}

+ 51 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/S3Uploader.java

@@ -0,0 +1,51 @@
+package org.apache.ambari.infra.job.archive;
+
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.s3.AmazonS3Client;
+
+import java.io.File;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+public class S3Uploader implements FileAction {
+
+  private final AmazonS3Client client;
+  private final String keyPrefix;
+  private final String bucketName;
+
+  public S3Uploader(S3Properties s3Properties) {
+    this.keyPrefix = s3Properties.getKeyPrefix();
+    this.bucketName = s3Properties.getBucketName();
+    BasicAWSCredentials credentials = new BasicAWSCredentials(s3Properties.getAccessKey(), s3Properties.getSecretKey());
+    client = new AmazonS3Client(credentials);
+  }
+
+  @Override
+  public File perform(File inputFile) {
+    String key = keyPrefix + inputFile.getName();
+
+    if (client.doesObjectExist(bucketName, key)) {
+      System.out.println("Object '" + key + "' already exists");
+      System.exit(0);
+    }
+
+    client.putObject(bucketName, key, inputFile);
+    return inputFile;
+  }
+}

+ 90 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrDocumentIterator.java

@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.SolrDocument;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.TimeZone;
+
+public class SolrDocumentIterator implements DocumentIterator {
+
+  private static final DateFormat SOLR_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
+
+  static {
+    SOLR_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+  }
+
+  private final Iterator<SolrDocument> documentIterator;
+  private final CloudSolrClient client;
+
+
+  public SolrDocumentIterator(QueryResponse response, CloudSolrClient client) {
+    documentIterator = response.getResults().iterator();
+    this.client = client;
+  }
+
+  @Override
+  public Document next() {
+    if (!documentIterator.hasNext())
+      return null;
+    
+    SolrDocument document = documentIterator.next();
+    HashMap<String, String> fieldMap = new HashMap<>();
+    for (String key : document.getFieldNames()) {
+      fieldMap.put(key, toString(document.get(key)));
+    }
+
+    return new Document(fieldMap);
+  }
+
+  private String toString(Object value) {
+    if (value == null) {
+      return null;
+    }
+    else if (value instanceof Date) {
+      return SOLR_DATE_FORMAT.format(value);
+    }
+    else {
+      return value.toString();
+    }
+  }
+
+  @Override
+  public void close() {
+    try {
+      client.close();
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  @Override
+  public boolean hasNext() {
+    return documentIterator.hasNext();
+  }
+}

+ 68 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrDocumentSource.java

@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.format.DateTimeFormatter;
+
+public class SolrDocumentSource implements DocumentSource {
+  public static final DateTimeFormatter SOLR_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
+  private static final Logger LOG = LoggerFactory.getLogger(SolrDocumentSource.class);
+
+  private final String zkHost;
+  private final SolrQueryProperties properties;
+  private final String endValue;
+
+  public SolrDocumentSource(String zkHost, SolrQueryProperties properties, String endValue) {
+    this.zkHost = zkHost;
+    this.properties = properties;
+    this.endValue = endValue;
+  }
+
+  @Override
+  public DocumentIterator open(Document current, int rows) {
+    CloudSolrClient client = new CloudSolrClient.Builder().withZkHost(zkHost).build();
+    client.setDefaultCollection(properties.getCollection());
+
+    SolrQuery query = properties.toQueryBuilder()
+            .setEndValue(endValue)
+            .setDocument(current)
+            .build();
+    query.setRows(rows);
+
+    LOG.info("Executing solr query {}", query.toLocalParamsString());
+
+    try {
+      QueryResponse response = client.query(query);
+      return new SolrDocumentIterator(response, client);
+    } catch (SolrServerException e) {
+      throw new RuntimeException(e);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+}

+ 115 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrQueryBuilder.java

@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.apache.solr.client.solrj.SolrQuery;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.apache.solr.client.solrj.SolrQuery.ORDER.asc;
+
+public class SolrQueryBuilder {
+
+  public static final Pattern PARAMETER_PATTERN = Pattern.compile("\\$\\{[a-z]+\\}");
+
+  private String queryText;
+  private String endValue;
+  private String filterQueryText;
+  private Document document;
+  private String[] sortFields;
+
+  public SolrQueryBuilder() {
+    this.queryText = "*:*";
+  }
+
+  public SolrQueryBuilder setQueryText(String queryText) {
+    this.queryText = queryText;
+    return this;
+  }
+
+  public SolrQueryBuilder setEndValue(String endValue) {
+    this.endValue = endValue;
+    return this;
+  }
+
+  public SolrQueryBuilder setFilterQueryText(String filterQueryText) {
+    this.filterQueryText = filterQueryText;
+    return this;
+  }
+
+
+  public SolrQueryBuilder setDocument(Document document) {
+    this.document = document;
+    return this;
+  }
+
+  public SolrQueryBuilder addSort(String... sortBy) {
+    this.sortFields = sortBy;
+    return this;
+  }
+
+  public SolrQuery build() {
+    SolrQuery solrQuery = new SolrQuery();
+
+    String query = queryText;
+    query = setEndValueOn(query);
+
+    solrQuery.setQuery(query);
+
+    if (filterQueryText != null) {
+      String filterQuery = filterQueryText;
+      filterQuery = setEndValueOn(filterQuery);
+
+      Set<String> paramNames = collectParamNames(filterQuery);
+      if (document != null) {
+        for (String parameter : paramNames) {
+          if (document.get(parameter) != null)
+            filterQuery = filterQuery.replace(String.format("${%s}", parameter), document.get(parameter));
+        }
+      }
+
+      if (document == null && paramNames.isEmpty() || document != null && !paramNames.isEmpty())
+        solrQuery.setFilterQueries(filterQuery);
+    }
+
+    if (sortFields != null) {
+      for (String field : sortFields)
+        solrQuery.addSort(field, asc);
+    }
+
+    return solrQuery;
+  }
+
+  private String setEndValueOn(String query) {
+    if (endValue != null)
+      query = query.replace("${end}", endValue);
+    return query;
+  }
+
+  private Set<String> collectParamNames(String filterQuery) {
+    Matcher matcher = PARAMETER_PATTERN.matcher(filterQuery);
+    Set<String> parameters = new HashSet<>();
+    while (matcher.find())
+      parameters.add(matcher.group().replace("${", "").replace("}", ""));
+    return parameters;
+  }
+}

+ 69 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/SolrQueryProperties.java

@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+public class SolrQueryProperties {
+  @NotBlank
+  private String collection;
+  @NotBlank
+  private String queryText;
+  private String filterQueryText;
+  private String[] sort;
+
+  public String getCollection() {
+    return collection;
+  }
+
+  public void setCollection(String collection) {
+    this.collection = collection;
+  }
+
+  public String getQueryText() {
+    return queryText;
+  }
+
+  public void setQueryText(String queryText) {
+    this.queryText = queryText;
+  }
+
+  public String getFilterQueryText() {
+    return filterQueryText;
+  }
+
+  public void setFilterQueryText(String filterQueryText) {
+    this.filterQueryText = filterQueryText;
+  }
+
+  public String[] getSort() {
+    return sort;
+  }
+
+  public void setSort(String[] sort) {
+    this.sort = sort;
+  }
+
+  public SolrQueryBuilder toQueryBuilder() {
+    return new SolrQueryBuilder().
+            setQueryText(queryText)
+            .setFilterQueryText(filterQueryText)
+            .addSort(sort);
+  }
+}

+ 50 - 0
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/job/archive/TarGzCompressor.java

@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+import org.apache.commons.io.IOUtils;
+
+import java.io.*;
+
+public class TarGzCompressor implements FileAction {
+  @Override
+  public File perform(File inputFile) {
+    File tarGzFile = new File(inputFile.getParent(), inputFile.getName() + ".tar.gz");
+    try (TarArchiveOutputStream tarArchiveOutputStream = new TarArchiveOutputStream(
+            new GzipCompressorOutputStream(new FileOutputStream(tarGzFile)))) {
+      TarArchiveEntry archiveEntry = new TarArchiveEntry(inputFile.getName());
+      archiveEntry.setSize(inputFile.length());
+      tarArchiveOutputStream.putArchiveEntry(archiveEntry);
+
+      try (FileInputStream fileInputStream = new FileInputStream(inputFile)) {
+        IOUtils.copy(fileInputStream, tarArchiveOutputStream);
+      }
+
+      tarArchiveOutputStream.closeArchiveEntry();
+    }
+    catch (IOException ex) {
+      throw new UncheckedIOException(ex);
+    }
+
+    return tarGzFile;
+  }
+}

+ 13 - 8
ambari-infra/ambari-infra-manager/src/main/java/org/apache/ambari/infra/manager/JobManager.java

@@ -18,6 +18,7 @@
  */
 package org.apache.ambari.infra.manager;
 
+import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import org.apache.ambari.infra.model.ExecutionContextResponse;
 import org.apache.ambari.infra.model.JobDetailsResponse;
@@ -28,16 +29,14 @@ import org.apache.ambari.infra.model.JobOperationParams;
 import org.apache.ambari.infra.model.StepExecutionContextResponse;
 import org.apache.ambari.infra.model.StepExecutionInfoResponse;
 import org.apache.ambari.infra.model.StepExecutionProgressResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.batch.admin.history.StepExecutionHistory;
 import org.springframework.batch.admin.service.JobService;
 import org.springframework.batch.admin.service.NoSuchStepExecutionException;
 import org.springframework.batch.admin.web.JobInfo;
 import org.springframework.batch.admin.web.StepExecutionProgress;
-import org.springframework.batch.core.JobExecution;
-import org.springframework.batch.core.JobInstance;
-import org.springframework.batch.core.JobParametersBuilder;
-import org.springframework.batch.core.JobParametersInvalidException;
-import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.*;
 import org.springframework.batch.core.launch.JobExecutionNotRunningException;
 import org.springframework.batch.core.launch.JobInstanceAlreadyExistsException;
 import org.springframework.batch.core.launch.JobOperator;
@@ -54,7 +53,6 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -64,6 +62,8 @@ import java.util.TimeZone;
 @Named
 public class JobManager {
 
+  private static final Logger LOG = LoggerFactory.getLogger(JobManager.class);
+
   @Inject
   private JobService jobService;
 
@@ -83,9 +83,14 @@ public class JobManager {
   public JobExecutionInfoResponse launchJob(String jobName, String params)
     throws JobParametersInvalidException, JobInstanceAlreadyExistsException, NoSuchJobException,
     JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
-    // TODO: handle params
     JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();
-    jobParametersBuilder.addDate("date", new Date());
+    if (params != null) {
+      LOG.info("Parsing parameters of job {} '{}'", jobName, params);
+      Splitter.on(',')
+              .trimResults()
+              .withKeyValueSeparator(Splitter.on('=').limit(2).trimResults())
+              .split(params).entrySet().forEach(entry -> jobParametersBuilder.addString(entry.getKey(), entry.getValue()));
+    }
     return new JobExecutionInfoResponse(jobService.launch(jobName, jobParametersBuilder.toJobParameters()), timeZone);
   }
 

+ 12 - 0
ambari-infra/ambari-infra-manager/src/main/resources/infra-manager.properties

@@ -18,3 +18,15 @@ infra-manager.batch.db.username=admin
 infra-manager.batch.db.password=admin
 management.security.enabled=false
 management.health.solr.enabled=false
+infra-manager.server.data.folder=/tmp
+
+infra-manager.jobs.solr_data_export.zoo_keeper_socket=zookeeper:2181
+infra-manager.jobs.solr_data_export.read_block_size=100
+infra-manager.jobs.solr_data_export.write_block_size=150
+infra-manager.jobs.solr_data_export.file_name_suffix_column=logtime
+infra-manager.jobs.solr_data_export.destination_directory_path=/tmp/ambariInfraManager
+infra-manager.jobs.solr_data_export.query.collection=hadoop_logs
+infra-manager.jobs.solr_data_export.query.query_text=logtime:[* TO "${end}"]
+infra-manager.jobs.solr_data_export.query.filter_query_text=(logtime:"${logtime}" AND id:{"${id}" TO *]) OR logtime:{"${logtime}" TO "${end}"]
+infra-manager.jobs.solr_data_export.query.sort[0]=logtime
+infra-manager.jobs.solr_data_export.query.sort[1]=id

+ 1 - 1
ambari-infra/ambari-infra-manager/src/main/resources/log4j2.xml

@@ -17,7 +17,7 @@
 -->
 <Configuration monitorinterval="30" status="info" strict="true">
   <Properties>
-    <Property name="logging.file">out/infra-manager.log</Property>
+    <Property name="logging.file">target/log/infra-manager.log</Property>
   </Properties>
   <Appenders>
     <Appender type="Console" name="Console">

+ 147 - 0
ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/DocumentExporterTest.java

@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.infra.job.archive;
+
+import org.easymock.EasyMockRunner;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.core.scope.context.StepContext;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamReader;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.HashMap;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+
+@RunWith(EasyMockRunner.class)
+public class DocumentExporterTest extends EasyMockSupport {
+
+  private DocumentExporter documentExporter;
+  @Mock
+  private ItemStreamReader<Document> reader;
+  @Mock
+  private DocumentDestination documentDestination;
+  @Mock
+  private DocumentItemWriter documentItemWriter;
+  @Mock
+  private DocumentItemWriter documentItemWriter2;
+
+  private ExecutionContext executionContext;
+  private ChunkContext chunkContext;
+  private static final Document DOCUMENT = new Document(new HashMap<String, String>() {{ put("id", "1"); }});
+
+  @Before
+  public void setUp() throws Exception {
+    StepExecution stepExecution = new StepExecution("exportDoc", new JobExecution(1L));
+    chunkContext = new ChunkContext(new StepContext(stepExecution));
+    executionContext = stepExecution.getExecutionContext();
+    documentExporter = new DocumentExporter(reader, documentDestination, 2);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    verifyAll();
+  }
+
+  @Test
+  public void testNothingToRead() throws Exception {
+    reader.open(executionContext); expectLastCall();
+    expect(reader.read()).andReturn(null);
+    reader.close(); expectLastCall();
+    replayAll();
+
+    documentExporter.execute(null, chunkContext);
+  }
+
+  @Test
+  public void testWriteLessDocumentsThanWriteBlockSize() throws Exception {
+    reader.open(executionContext); expectLastCall();
+    expect(reader.read()).andReturn(DOCUMENT);
+    expect(documentDestination.open(DOCUMENT)).andReturn(documentItemWriter);
+    documentItemWriter.write(DOCUMENT); expectLastCall();
+    expect(reader.read()).andReturn(null);
+    reader.close(); expectLastCall();
+    documentItemWriter.close(); expectLastCall();
+    replayAll();
+
+    documentExporter.execute(null, chunkContext);
+  }
+
+  @Test
+  public void testWriteMoreDocumentsThanWriteBlockSize() throws Exception {
+    Document document2 = new Document(new HashMap<String, String>() {{ put("id", "2"); }});
+    Document document3 = new Document(new HashMap<String, String>() {{ put("id", "3"); }});
+
+    reader.open(executionContext); expectLastCall();
+    expect(reader.read()).andReturn(DOCUMENT);
+    expect(documentDestination.open(DOCUMENT)).andReturn(documentItemWriter);
+    documentItemWriter.write(DOCUMENT); expectLastCall();
+    expect(reader.read()).andReturn(document2);
+    documentItemWriter.write(document2); expectLastCall();
+    expect(reader.read()).andReturn(document3);
+    documentItemWriter.close(); expectLastCall();
+    expect(documentDestination.open(document3)).andReturn(documentItemWriter2);
+    documentItemWriter2.write(document3); expectLastCall();
+    expect(reader.read()).andReturn(null);
+    reader.update(executionContext);
+    reader.close(); expectLastCall();
+    documentItemWriter2.close(); expectLastCall();
+    replayAll();
+
+    documentExporter.execute(null, chunkContext);
+  }
+
+  @Test(expected = IOException.class)
+  public void testReadError() throws Exception {
+    reader.open(executionContext); expectLastCall();
+    expect(reader.read()).andReturn(DOCUMENT);
+    expect(documentDestination.open(DOCUMENT)).andReturn(documentItemWriter);
+    documentItemWriter.write(DOCUMENT); expectLastCall();
+    expect(reader.read()).andThrow(new IOException("TEST"));
+    documentItemWriter.revert(); expectLastCall();
+    reader.close(); expectLastCall();
+    replayAll();
+
+    documentExporter.execute(null, chunkContext);
+  }
+
+  @Test(expected = UncheckedIOException.class)
+  public void testWriteError() throws Exception {
+    reader.open(executionContext); expectLastCall();
+    expect(reader.read()).andReturn(DOCUMENT);
+    expect(documentDestination.open(DOCUMENT)).andReturn(documentItemWriter);
+    documentItemWriter.write(DOCUMENT); expectLastCall().andThrow(new UncheckedIOException(new IOException("TEST")));
+    documentItemWriter.revert(); expectLastCall();
+    reader.close(); expectLastCall();
+    replayAll();
+
+    documentExporter.execute(null, chunkContext);
+  }
+}

+ 197 - 0
ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/DocumentItemReaderTest.java

@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.infra.job.archive;
+
+import org.easymock.EasyMockRunner;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.item.ExecutionContext;
+
+import java.util.HashMap;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+
+@RunWith(EasyMockRunner.class)
+public class DocumentItemReaderTest extends EasyMockSupport {
+  private static final Document DOCUMENT = new Document(new HashMap<String, String>() {{ put("id", "1"); }});
+  private static final Document DOCUMENT_2 = new Document(new HashMap<String, String>() {{ put("id", "2"); }});
+  private static final Document DOCUMENT_3 = new Document(new HashMap<String, String>() {{ put("id", "3"); }});
+  private static final int READ_BLOCK_SIZE = 2;
+
+  private DocumentItemReader documentItemReader;
+  @Mock
+  private DocumentSource documentSource;
+  @Mock
+  private DocumentIterator documentIterator;
+  @Mock
+  private DocumentIterator documentIterator2;
+
+  @Before
+  public void setUp() throws Exception {
+    documentItemReader = new DocumentItemReader(documentSource, READ_BLOCK_SIZE);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    verifyAll();
+  }
+
+  @Test
+  public void testReadWhenCollectionIsEmpty() throws Exception {
+    expect(documentSource.open(null, 2)).andReturn(documentIterator);
+    expect(documentIterator.next()).andReturn(null);
+    documentIterator.close(); expectLastCall();
+    replayAll();
+
+    assertThat(documentItemReader.read(), is(nullValue()));
+    assertThat(documentItemReader.isComplete(null), is(true));
+    assertThat(documentItemReader.isComplete(null, null), is(true));
+  }
+
+  @Test
+  public void testReadWhenCollectionContainsLessElementsThanReadBlockSize() throws Exception {
+    expect(documentSource.open(null, 2)).andReturn(documentIterator);
+    expect(documentIterator.next()).andReturn(DOCUMENT);
+    expect(documentIterator.next()).andReturn(null);
+    documentIterator.close(); expectLastCall();
+    replayAll();
+
+    assertThat(documentItemReader.read(), is(DOCUMENT));
+    assertThat(documentItemReader.isComplete(null), is(false));
+    assertThat(documentItemReader.isComplete(null, null), is(false));
+    assertThat(documentItemReader.read(), is(nullValue()));
+    assertThat(documentItemReader.isComplete(null), is(true));
+    assertThat(documentItemReader.isComplete(null, null), is(true));
+  }
+
+  @Test
+  public void testReadWhenCollectionContainsExactlySameCountElementsAsReadBlockSize() throws Exception {
+    expect(documentSource.open(null, 2)).andReturn(documentIterator);
+    expect(documentSource.open(DOCUMENT_2, 2)).andReturn(documentIterator2);
+    expect(documentIterator.next()).andReturn(DOCUMENT);
+    expect(documentIterator.next()).andReturn(DOCUMENT_2);
+    expect(documentIterator.next()).andReturn(null);
+    documentIterator.close(); expectLastCall();
+
+    expect(documentIterator2.next()).andReturn(null);
+    documentIterator2.close(); expectLastCall();
+    replayAll();
+
+    assertThat(documentItemReader.read(), is(DOCUMENT));
+    assertThat(documentItemReader.isComplete(null), is(false));
+    assertThat(documentItemReader.isComplete(null, null), is(false));
+    assertThat(documentItemReader.read(), is(DOCUMENT_2));
+    assertThat(documentItemReader.isComplete(null), is(false));
+    assertThat(documentItemReader.isComplete(null, null), is(false));
+    assertThat(documentItemReader.read(), is(nullValue()));
+    assertThat(documentItemReader.isComplete(null), is(true));
+    assertThat(documentItemReader.isComplete(null, null), is(true));
+  }
+
+  @Test
+  public void testReadWhenCollectionContainsMoreElementsThanReadBlockSize() throws Exception {
+    Document document3 = new Document(new HashMap<String, String>() {{ put("id", "2"); }});
+
+    expect(documentSource.open(null, 2)).andReturn(documentIterator);
+    expect(documentSource.open(DOCUMENT_2, 2)).andReturn(documentIterator2);
+    expect(documentIterator.next()).andReturn(DOCUMENT);
+    expect(documentIterator.next()).andReturn(DOCUMENT_2);
+    expect(documentIterator.next()).andReturn(null);
+    documentIterator.close(); expectLastCall();
+    expect(documentIterator2.next()).andReturn(document3);
+    expect(documentIterator2.next()).andReturn(null);
+    documentIterator2.close(); expectLastCall();
+
+    replayAll();
+
+    assertThat(documentItemReader.read(), is(DOCUMENT));
+    assertThat(documentItemReader.isComplete(null), is(false));
+    assertThat(documentItemReader.isComplete(null, null), is(false));
+
+    assertThat(documentItemReader.read(), is(DOCUMENT_2));
+    assertThat(documentItemReader.isComplete(null), is(false));
+    assertThat(documentItemReader.isComplete(null, null), is(false));
+
+    assertThat(documentItemReader.read(), is(document3));
+    assertThat(documentItemReader.isComplete(null), is(false));
+    assertThat(documentItemReader.isComplete(null, null), is(false));
+
+    assertThat(documentItemReader.read(), is(nullValue()));
+    assertThat(documentItemReader.isComplete(null), is(true));
+    assertThat(documentItemReader.isComplete(null, null), is(true));
+  }
+
+  @Test
+  public void testContinueWhenOnlyFirstElementWasRead() throws Exception {
+    expect(documentSource.open(null, 2)).andReturn(documentIterator);
+    expect(documentIterator.next()).andReturn(DOCUMENT);
+    documentIterator.close(); expectLastCall();
+    expect(documentSource.open(null, 2)).andReturn(documentIterator2);
+    expect(documentIterator2.next()).andReturn(DOCUMENT);
+    documentIterator2.close(); expectLastCall();
+    replayAll();
+
+    ExecutionContext executionContext = new ExecutionContext();
+    documentItemReader.open(executionContext);
+    assertThat(documentItemReader.read(), is(DOCUMENT));
+    documentItemReader.update(executionContext);
+    assertThat(executionContext.containsKey(DocumentItemReader.POSITION), is(false));
+    documentItemReader.close();
+
+    documentItemReader.open(executionContext);
+    assertThat(documentItemReader.read(), is(DOCUMENT));
+    documentItemReader.close();
+  }
+
+  @Test
+  public void testContinueWhenMoreThanOneElementWasRead() throws Exception {
+    expect(documentSource.open(null, 2)).andReturn(documentIterator);
+    expect(documentIterator.next()).andReturn(DOCUMENT);
+    expect(documentIterator.next()).andReturn(DOCUMENT_2);
+    documentIterator.close(); expectLastCall();
+    expect(documentSource.open(DOCUMENT, 2)).andReturn(documentIterator2);
+    expect(documentIterator2.next()).andReturn(DOCUMENT_2);
+    expect(documentIterator2.next()).andReturn(DOCUMENT_3);
+    documentIterator2.close(); expectLastCall();
+
+    replayAll();
+
+    ExecutionContext executionContext = new ExecutionContext();
+    documentItemReader.open(executionContext);
+    assertThat(documentItemReader.read(), is(DOCUMENT));
+    assertThat(documentItemReader.read(), is(DOCUMENT_2));
+    documentItemReader.update(executionContext);
+    assertThat(executionContext.get(DocumentItemReader.POSITION), is(DOCUMENT));
+    documentItemReader.close();
+
+    documentItemReader.open(executionContext);
+    assertThat(documentItemReader.read(), is(DOCUMENT_2));
+    assertThat(documentItemReader.read(), is(DOCUMENT_3));
+    documentItemReader.close();
+  }
+}

+ 98 - 0
ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/LocalDocumentItemWriterTest.java

@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.infra.job.archive;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.FileUtils;
+import org.easymock.EasyMockRunner;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import static org.easymock.EasyMock.expect;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+@RunWith(EasyMockRunner.class)
+public class LocalDocumentItemWriterTest extends EasyMockSupport {
+
+  private static final Document DOCUMENT = new Document(new HashMap<String, String>() {{ put("id", "1"); }});
+  private static final Document DOCUMENT2 = new Document(new HashMap<String, String>() {{ put("id", "2"); }});
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  private LocalDocumentItemWriter localDocumentItemWriter;
+  private File outFile;
+  @Mock
+  private FileAction fileAction;
+
+  @Before
+  public void setUp() throws Exception {
+    outFile = File.createTempFile("LocalDocumentItemWriterTest", "json.tmp");
+    localDocumentItemWriter = new LocalDocumentItemWriter(outFile, fileAction);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    outFile.delete();
+    verifyAll();
+  }
+
+  @Test
+  public void testWrite() throws Exception {
+    expect(fileAction.perform(outFile)).andReturn(outFile);
+    replayAll();
+
+    localDocumentItemWriter.write(DOCUMENT);
+    localDocumentItemWriter.write(DOCUMENT2);
+    localDocumentItemWriter.close();
+
+    List<Document> documentList = readBack(outFile);
+    assertThat(documentList.size(), is(2));
+    assertThat(documentList.get(0).get("id"), is(DOCUMENT.get("id")));
+    assertThat(documentList.get(1).get("id"), is(DOCUMENT2.get("id")));
+  }
+
+  private List<Document> readBack(File file) throws IOException {
+    List<Document> documentList = new ArrayList<>();
+    for (String line : FileUtils.readLines(file)) {
+      documentList.add(OBJECT_MAPPER.readValue(line, Document.class));
+    }
+    return documentList;
+  }
+
+  @Test
+  public void testRevert() throws Exception {
+    replayAll();
+
+    localDocumentItemWriter.write(DOCUMENT);
+    localDocumentItemWriter.revert();
+
+    assertThat(outFile.exists(), is(false));
+  }
+}

+ 113 - 0
ambari-infra/ambari-infra-manager/src/test/java/org/apache/ambari/infra/job/archive/SolrQueryBuilderTest.java

@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.ambari.infra.job.archive;
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+
+import static org.apache.ambari.infra.job.archive.SolrQueryBuilder.PARAMETER_PATTERN;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertThat;
+
+public class SolrQueryBuilderTest {
+  private static final Document DOCUMENT = new Document(new HashMap<String, String>() {{
+    put("logtime", "2017-10-02'T'10:00:11.634Z");
+    put("id", "1");
+  }});
+
+  @Test
+  public void testDefaultQuery() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder()
+            .build();
+    assertThat(solrQuery.getQuery(), is("*:*"));
+  }
+
+  @Test
+  public void testSetQuery() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder()
+            .setQueryText("logtime:[* TO \"${end}\"]")
+            .setEndValue("2017-11-27'T'10:12:11.372Z")
+            .build();
+    assertThat(solrQuery.getQuery(), is("logtime:[* TO \"2017-11-27'T'10:12:11.372Z\"]"));
+  }
+
+  @Test
+  public void testSetFilterQuery() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder()
+            .setFilterQueryText("(logtime:\"${logtime}\" AND id:{\"${id}\" TO *]) OR logtime:{\"${logtime}\" TO \"${end}\"]")
+            .setDocument(DOCUMENT)
+            .setEndValue("2017-11-27'T'10:12:11.372Z")
+            .build();
+    assertThat(solrQuery.getFilterQueries()[0], is("(logtime:\"2017-10-02'T'10:00:11.634Z\" AND id:{\"1\" TO *]) OR logtime:{\"2017-10-02'T'10:00:11.634Z\" TO \"2017-11-27'T'10:12:11.372Z\"]"));
+  }
+
+  @Test
+  public void testSetFilterQueryWhenDocumentIsNull() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder()
+            .setFilterQueryText("(logtime:\"${logtime}\" AND id:{\"${id}\" TO *]) OR logtime:{\"${logtime}\" TO \"${end}\"]")
+            .setEndValue("2017-11-27'T'10:12:11.372Z")
+            .build();
+    assertThat(solrQuery.getFilterQueries(), is(nullValue()));
+  }
+
+  @Test
+  public void testSetFilterQueryWhenEndValueIsNull() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder()
+            .setFilterQueryText("logtime:\"${logtime}\" AND id:{\"${id}\" TO *]")
+            .setDocument(DOCUMENT)
+            .build();
+    assertThat(solrQuery.getFilterQueries()[0], is("logtime:\"2017-10-02'T'10:00:11.634Z\" AND id:{\"1\" TO *]"));
+  }
+
+  @Test
+  public void testSetFilterQueryWhenQueryFilterIsNullButDocumentIsNot() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder()
+            .setDocument(DOCUMENT)
+            .build();
+    assertThat(solrQuery.getFilterQueries(), is(nullValue()));
+  }
+
+  @Test
+  public void testRegex() throws Exception {
+    Matcher matcher = PARAMETER_PATTERN.matcher("(logtime:\"${logtime}\" AND id:{\"${id}\" TO *]) OR logtime:{\"${logtime}\" TO \"${end}\"]");
+    List<String> parameters = new ArrayList<>();
+    while (matcher.find())
+      parameters.add(matcher.group());
+
+    assertThat(parameters, hasSize(4));
+    assertThat(parameters.get(0), is("${logtime}"));
+    assertThat(parameters.get(1), is("${id}"));
+    assertThat(parameters.get(2), is("${logtime}"));
+    assertThat(parameters.get(3), is("${end}"));
+  }
+
+  @Test
+  public void testSort() throws Exception {
+    SolrQuery solrQuery = new SolrQueryBuilder().addSort("logtime", "id").build();
+    assertThat(solrQuery.getSorts().get(0).getItem(), is("logtime"));
+    assertThat(solrQuery.getSorts().get(1).getItem(), is("id"));
+  }
+}

+ 2 - 2
ambari-logsearch/ambari-logsearch-logfeeder/src/main/java/org/apache/ambari/logfeeder/filter/FilterKeyValue.java

@@ -128,14 +128,14 @@ public class FilterKeyValue extends Filter {
 
   private String[] getNameValue(String nv) {
     String splitPattern = Pattern.quote(valueSplit);
-    return nv.split(splitPattern);
+    return nv.split(splitPattern, 2);
   }
 
   private void logParseError(String inputStr) {
     errorMetric.value++;
     String logMessageKey = this.getClass().getSimpleName() + "_PARSEERROR";
     LogFeederUtil.logErrorMessageByInterval(logMessageKey, "Error parsing string. length=" + inputStr.length() + ", input=" +
-        input.getShortDescription() + ". First upto 100 characters=" + StringUtils.abbreviate(inputStr, 100), null, LOG,
+        input.getShortDescription() + ". First upto 200 characters=" + StringUtils.abbreviate(inputStr, 200), null, LOG,
         Level.ERROR);
   }
 

+ 5 - 10
ambari-logsearch/ambari-logsearch-server/src/main/java/org/apache/ambari/logsearch/web/filters/LogsearchAuthenticationEntryPoint.java

@@ -24,12 +24,13 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.log4j.Logger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
 
 public class LogsearchAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
-  private static final Logger logger = Logger.getLogger(LogsearchAuthenticationEntryPoint.class);
+  private static final Logger logger = LoggerFactory.getLogger(LogsearchAuthenticationEntryPoint.class);
 
   public LogsearchAuthenticationEntryPoint(String loginFormUrl) {
     super(loginFormUrl);
@@ -38,13 +39,7 @@ public class LogsearchAuthenticationEntryPoint extends LoginUrlAuthenticationEnt
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
     throws IOException, ServletException {
-    String ajaxRequestHeader = request.getHeader("X-Requested-With");
-    if (ajaxRequestHeader != null && ajaxRequestHeader.equalsIgnoreCase("XMLHttpRequest")) {
-      logger.debug("AJAX request. Authentication required. Returning URL=" + request.getRequestURI());
-      response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session Timeout");
-    } else {
-      logger.debug("Redirecting to login page :" + this.getLoginFormUrl());
-      super.commence(request, response, authException);
-    }
+    logger.debug("Got 401 from request: {}", request.getRequestURI());
+    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
   }
 }

+ 20 - 2
ambari-logsearch/ambari-logsearch-web/src/app/classes/filtering.ts

@@ -18,9 +18,10 @@
 
 import {Moment, unitOfTime} from 'moment';
 import {ListItem} from '@app/classes/list-item';
+import {TimeRangeType, SortingType} from '@app/classes/string';
 
 export interface TimeUnit {
-  type: 'CURRENT' | 'LAST' | 'PAST';
+  type: TimeRangeType;
   unit: unitOfTime.DurationConstructor;
   interval?: number;
 }
@@ -33,7 +34,7 @@ export interface CustomTimeRange {
 
 export interface SortingConditions {
   key: string;
-  type: 'asc' | 'desc';
+  type: SortingType;
 }
 
 export interface TimeUnitListItem extends ListItem {
@@ -49,4 +50,21 @@ export interface FilterCondition {
   options?: (ListItem | TimeUnitListItem[])[];
   defaultSelection?: ListItem | ListItem[] | number;
   iconClass?: string;
+  fieldName?: string;
+}
+
+export interface SearchBoxParameter {
+  name: string;
+  value: string;
+  isExclude: boolean;
+}
+
+export interface SearchBoxParameterProcessed extends SearchBoxParameter {
+  id: number;
+  label: string;
+}
+
+export interface SearchBoxParameterTriggered {
+  value: string;
+  isExclude: boolean;
 }

+ 2 - 1
ambari-logsearch/ambari-logsearch-web/src/app/classes/models/app-state.ts

@@ -17,12 +17,13 @@
  */
 
 import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry';
+import {LogsType} from '@app/classes/string';
 
 export interface AppState {
   isAuthorized: boolean;
   isInitialLoading: boolean;
   isLoginInProgress: boolean;
-  activeLogsType?: string;
+  activeLogsType?: LogsType;
   isServiceLogsFileView: boolean;
   isServiceLogContextView: boolean;
   activeLog: ActiveServiceLogEntry | null;

+ 2 - 1
ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/audit-logs-query-params.ts

@@ -17,6 +17,7 @@
  */
 
 import {QueryParams} from '@app/classes/queries/query-params';
+import {SortingType} from '@app/classes/string';
 
 export const defaultParams = {
   page: '0',
@@ -35,7 +36,7 @@ export class AuditLogsQueryParams extends QueryParams {
   pageSize: string;
   startIndex: string;
   sortBy?: string;
-  sortType?: 'asc' | 'desc';
+  sortType?: SortingType;
   clusters?: string;
   mustBe?: string;
   mustNot?: string;

+ 2 - 1
ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/service-logs-truncated-query-params.ts

@@ -17,6 +17,7 @@
  */
 
 import {QueryParams} from '@app/classes/queries/query-params';
+import {ScrollType} from '@app/classes/string';
 
 export const defaultParams = {
   numberRows: '10',
@@ -32,5 +33,5 @@ export class ServiceLogsTruncatedQueryParams extends QueryParams {
   host_name: string;
   component_name: string;
   numberRows: string;
-  scrollType: 'before' | 'after' | '';
+  scrollType: ScrollType;
 }

+ 25 - 0
ambari-logsearch/ambari-logsearch-web/src/app/classes/string.ts

@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+export type LogsType = 'auditLogs' | 'serviceLogs';
+
+export type TimeRangeType = 'CURRENT' | 'LAST' | 'PAST';
+
+export type SortingType = 'asc' | 'desc';
+
+export type ScrollType = 'before' | 'after' | '';

+ 0 - 3
ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less

@@ -15,13 +15,10 @@
  * limitations under the License.
  */
 
-@import '../variables';
-
 :host {
   display: block;
   margin-left: auto;
   menu-button {
     margin: 0 1em;
-    color: @table-border-color;
   }
 }

+ 0 - 4
ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts

@@ -30,7 +30,6 @@ export class ActionMenuComponent {
     {
       iconClass: 'fa fa-arrow-left',
       label: 'topMenu.undo',
-      labelClass: 'unstyled-link',
       action: 'undo',
       subItems: [
         {
@@ -50,7 +49,6 @@ export class ActionMenuComponent {
     {
       iconClass: 'fa fa-arrow-right',
       label: 'topMenu.redo',
-      labelClass: 'unstyled-link',
       action: 'redo',
       subItems: [
         {
@@ -67,13 +65,11 @@ export class ActionMenuComponent {
     {
       iconClass: 'fa fa-refresh',
       label: 'topMenu.refresh',
-      labelClass: 'unstyled-link',
       action: 'refresh'
     },
     {
       iconClass: 'fa fa-history',
       label: 'topMenu.history',
-      labelClass: 'unstyled-link',
       action: 'openHistory',
       isRightAlign: true,
       subItems: [

+ 5 - 3
ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.html

@@ -15,12 +15,14 @@
   limitations under the License.
 -->
 
-<dropdown-button class="pull-right" label="logs.columns" [options]="columns" [isRightAlign]="true"
+<dropdown-button class="pull-right" label="{{'logs.columns' | translate}}" [options]="columns" [isRightAlign]="true"
                  [isMultipleChoice]="true" action="updateSelectedColumns"
                  [additionalArgs]="logsTypeMapObject.fieldsModel"></dropdown-button>
 <form *ngIf="logs && logs.length" [formGroup]="filtersForm" class="row pull-right">
-  <filter-dropdown class="col-md-12" [label]="filters.auditLogsSorting.label" formControlName="auditLogsSorting"
-                   [options]="filters.auditLogsSorting.options" [isRightAlign]="true"></filter-dropdown></form>
+  <filter-dropdown class="col-md-12" label="{{filters.auditLogsSorting.label | translate}}"
+                   formControlName="auditLogsSorting" [options]="filters.auditLogsSorting.options"
+                   [isRightAlign]="true"></filter-dropdown>
+</form>
 <div class="panel panel-default">
   <div class="panel-body">
     <table class="table">

+ 2 - 2
ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.html

@@ -16,11 +16,11 @@
 -->
 
 <div [ngClass]="{'dropup': isDropup}">
-  <button class="btn btn-link dropdown-toggle" data-toggle="dropdown">
+  <button [ngClass]="['btn', 'btn-link', 'dropdown-toggle', buttonClass]" data-toggle="dropdown">
     <span *ngIf="iconClass || label"
           [ngClass]="{'filter-label': true, 'plain': !isMultipleChoice && !hideCaret && showSelectedValue}">
       <span *ngIf="iconClass" [ngClass]="iconClass"></span>
-      <span *ngIf="label">{{label | translate}}</span>
+      <span *ngIf="label">{{label}}</span>
     </span>
     <span *ngIf="showSelectedValue && !isMultipleChoice && selection.length">{{selection[0].label | translate}}</span>
     <span *ngIf="!hideCaret" class="caret"></span>

+ 8 - 9
ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.less

@@ -20,17 +20,16 @@
 :host {
   .default-flex;
   position: relative;
-  float: left;
 
-  .filter-label {
-    padding: @input-group-addon-padding;
+  button {
+    text-transform: none;
 
-    &.plain {
-      color: initial;
-    }
-  }
+    .filter-label {
+      padding: @input-group-addon-padding;
 
-  .btn {
-    text-transform: none;
+      &.plain {
+        color: initial;
+      }
+    }
   }
 }

+ 3 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts

@@ -34,6 +34,9 @@ export class DropdownButtonComponent {
   @Input()
   label?: string;
 
+  @Input()
+  buttonClass: string = '';
+
   @Input()
   iconClass?: string;
 

+ 8 - 10
ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html

@@ -17,22 +17,20 @@
 
 <form [formGroup]="filtersForm">
   <div class="form-inline filter-input-container col-md-8">
-    <filter-dropdown *ngIf="isFilterConditionDisplayed('clusters')" [label]="filters.clusters.label"
-                     formControlName="clusters" [options]="filters.clusters.options" [isMultipleChoice]="true"
-                     class="filter-input"></filter-dropdown>
-    <search-box formControlName="query" [items]="searchBoxItemsTranslated" class="filter-input"
-                [parameterNameChangeSubject]="queryParameterNameChange"
-                [parameterAddSubject]="queryParameterAdd"></search-box>
+    <search-box [parameterAddSubject]="queryParameterAdd" [parameterNameChangeSubject]="queryParameterNameChange"
+                formControlName="query" [items]="searchBoxItemsTranslated" [itemsOptions]="options"
+                [updateValueImmediately]="false" [updateValueSubject]="searchBoxValueUpdate" class="filter-input"></search-box>
     <time-range-picker *ngIf="isFilterConditionDisplayed('timeRange')" formControlName="timeRange"
                        class="filter-input"></time-range-picker>
     <timezone-picker class="filter-input"></timezone-picker>
-    <!--button class="btn btn-success" type="button">
+    <button class="btn btn-success search-button" type="button" (click)="updateSearchBoxValue()">
       <span class="fa fa-search"></span>
-    </button-->
+    </button>
   </div>
   <div class="filter-buttons col-md-4">
-    <dropdown-button [options]="searchBoxItems" iconClass="fa fa-search-minus" label="filter.excluded"
-                     [hideCaret]="true" [showSelectedValue]="false" action="proceedWithExclude"></dropdown-button>
+    <dropdown-button [options]="searchBoxItems | async" iconClass="fa fa-search-minus" action="proceedWithExclude"
+                     label="{{'filter.exclude' | translate}}" [hideCaret]="true"
+                     [showSelectedValue]="false"></dropdown-button>
     <filter-button *ngIf="isFilterConditionDisplayed('hosts')" formControlName="hosts"
                    label="{{filters.hosts.label | translate}}" [iconClass]="filters.hosts.iconClass"
                    [subItems]="filters.hosts.options" [isMultipleChoice]="true" [isRightAlign]="true"

+ 8 - 3
ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.less

@@ -28,9 +28,14 @@
     align-items: flex-start;
     justify-content: flex-start;
 
-    .btn-success {
-      border-top-left-radius: 0;
-      border-bottom-left-radius: 0;
+    .search-button {
+      border: 1px solid @submit-color;
+      height: auto;
+
+      &:last-child {
+        border-top-left-radius: 0;
+        border-bottom-left-radius: 0;
+      }
     }
 
     .filter-input {

+ 1 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.spec.ts

@@ -106,6 +106,7 @@ describe('FiltersPanelComponent', () => {
     component.filtersForm = new FormGroup({
       control: new FormControl()
     });
+    component.logsType = 'auditLogs';
     fixture.detectChanges();
   });
 

+ 70 - 50
ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts

@@ -16,79 +16,94 @@
  * limitations under the License.
  */
 
-import {Component, Input} from '@angular/core';
+import {Component, OnChanges, SimpleChanges, Input} from '@angular/core';
 import {FormGroup} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
 import {Subject} from 'rxjs/Subject';
-import {TranslateService} from '@ngx-translate/core';
+import 'rxjs/add/observable/from';
+import {FilterCondition, SearchBoxParameter, SearchBoxParameterTriggered} from '@app/classes/filtering';
 import {ListItem} from '@app/classes/list-item';
+import {LogsType} from '@app/classes/string';
 import {CommonEntry} from '@app/classes/models/common-entry';
-import {LogField} from '@app/classes/models/log-field';
 import {LogsContainerService} from '@app/services/logs-container.service';
-import {AppStateService} from '@app/services/storage/app-state.service';
 
 @Component({
   selector: 'filters-panel',
   templateUrl: './filters-panel.component.html',
   styleUrls: ['./filters-panel.component.less']
 })
-export class FiltersPanelComponent {
-
-  constructor(
-    private translate: TranslateService, private logsContainer: LogsContainerService,
-    private appState: AppStateService
-  ) {
-    appState.getParameter('activeLogsType').subscribe(value => {
-      this.logsType = value;
-      logsContainer.logsTypeMap[value].fieldsModel.getAll().subscribe((fields: LogField[]): void => {
-        if (fields.length) {
-          const items = fields.filter((field: LogField): boolean => {
-              return this.excludedParameters.indexOf(field.name) === -1;
-            }).map((field: LogField): CommonEntry => {
-              return {
-                name: field.displayName || field.name,
-                value: field.name
-              };
-            }),
-            labelKeys = items.map((item: CommonEntry): string => item.name);
-          this.searchBoxItems = items.map((item: CommonEntry): ListItem => {
-            return {
-              label: item.name,
-              value: item.value
-            };
-          });
-          translate.get(labelKeys).first().subscribe((translation: {[key: string]: string}): void => {
-            this.searchBoxItemsTranslated = items.map((item: CommonEntry): CommonEntry => {
-              return {
-                name: translation[item.name],
-                value: item.value
-              };
-            })
-          });
-        }
-      })
-    });
+export class FiltersPanelComponent implements OnChanges {
+
+  constructor(private logsContainer: LogsContainerService) {
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.hasOwnProperty('logsType')) {
+      let result;
+      switch (changes.logsType.currentValue) {
+        case 'auditLogs':
+          result = this.logsContainer.auditLogsColumns;
+          break;
+        case 'serviceLogs':
+          result = this.logsContainer.serviceLogsColumns;
+          break;
+        default:
+          result = Observable.from([]);
+          break;
+      }
+      this.searchBoxItems = result;
+    }
   }
 
   @Input()
   filtersForm: FormGroup;
 
-  private readonly excludedParameters = ['cluster', 'host', 'level', 'type', 'logtime'];
-
-  private logsType: string;
+  @Input()
+  logsType: LogsType;
 
-  searchBoxItems: ListItem[] = [];
+  searchBoxItems: Observable<ListItem[]>;
 
-  searchBoxItemsTranslated: CommonEntry[] = [];
+  get searchBoxItemsTranslated(): CommonEntry[] {
+    switch (this.logsType) {
+      case 'auditLogs':
+        return this.logsContainer.auditLogsColumnsTranslated;
+      case 'serviceLogs':
+        return this.logsContainer.serviceLogsColumnsTranslated;
+      default:
+        return [];
+    }
+  }
 
-  get filters(): any {
+  get filters(): {[key: string]: FilterCondition} {
     return this.logsContainer.filters;
   }
 
-  get queryParameterNameChange(): Subject<any> {
+  /**
+   * Object with options for search box parameter values
+   * @returns {[key: string]: CommonEntry[]}
+   */
+  get options(): {[key: string]: CommonEntry[]} {
+    return Object.keys(this.filters).filter((key: string): boolean => {
+      const condition = this.filters[key];
+      return Boolean(condition.fieldName && condition.options);
+    }).reduce((currentValue, currentKey) => {
+      const condition = this.filters[currentKey];
+      return Object.assign(currentValue, {
+        [condition.fieldName]: condition.options.map((option: ListItem): CommonEntry => {
+          return {
+            name: option.value,
+            value: option.value
+          }
+        })
+      });
+    }, {});
+  }
+
+  get queryParameterNameChange(): Subject<SearchBoxParameterTriggered> {
     return this.logsContainer.queryParameterNameChange;
   }
 
-  get queryParameterAdd(): Subject<any> {
+  get queryParameterAdd(): Subject<SearchBoxParameter> {
     return this.logsContainer.queryParameterAdd;
   }
 
@@ -96,9 +111,14 @@ export class FiltersPanelComponent {
     return this.logsContainer.captureSeconds;
   }
 
+  searchBoxValueUpdate: Subject<void> = new Subject();
+
   isFilterConditionDisplayed(key: string): boolean {
-    return this.logsContainer.logsTypeMap[this.logsType].listFilters.indexOf(key) > -1
-      && Boolean(this.filtersForm.controls[key]);
+    return this.logsContainer.isFilterConditionDisplayed(key);
+  }
+
+  updateSearchBoxValue(): void {
+    this.searchBoxValueUpdate.next();
   }
 
 }

+ 1 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html

@@ -25,7 +25,7 @@
   </div>
 </div>
 <div class="container-fluid">
-  <filters-panel class="row" [filtersForm]="filtersForm"></filters-panel>
+  <filters-panel class="row" [filtersForm]="filtersForm" [logsType]="logsType"></filters-panel>
   <div class="row">
     <div *ngIf="autoRefreshRemainingSeconds" class="col-md-12">
       <div class="auto-refresh-message pull-right">

+ 3 - 2
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts

@@ -30,6 +30,7 @@ import {BarGraph} from '@app/classes/models/bar-graph';
 import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry';
 import {HistogramOptions} from '@app/classes/histogram-options';
 import {ListItem} from '@app/classes/list-item';
+import {LogsType} from '@app/classes/string';
 
 @Component({
   selector: 'logs-container',
@@ -43,7 +44,7 @@ export class LogsContainerComponent {
     private tabsStorage: TabsService, private logsContainer: LogsContainerService
   ) {
     this.logsContainer.loadColumnsNames();
-    appState.getParameter('activeLogsType').subscribe((value: string) => this.logsType = value);
+    appState.getParameter('activeLogsType').subscribe((value: LogsType) => this.logsType = value);
     serviceLogsHistogramStorage.getAll().subscribe((data: BarGraph[]): void => {
       this.histogramData = this.logsContainer.getHistogramData(data);
     });
@@ -56,7 +57,7 @@ export class LogsContainerComponent {
     return this.logsContainer.filtersForm;
   };
 
-  private logsType: string;
+  private logsType: LogsType;
 
   get totalCount(): number {
     return this.logsContainer.totalCount;

+ 2 - 11
ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less

@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-@import '../variables';
+@import '../mixins';
 
 :host {
   cursor: pointer;
@@ -24,8 +24,7 @@
   a {
     text-align: center;
     text-decoration: none;
-    i {
-      color: @link-color;
+    .icon {
       display: inline-block;
       position: relative;
       &.fa-caret-down {
@@ -36,14 +35,6 @@
       display: block;
     }
   }
-  a:hover, a:focus {
-    i {
-      color: @link-hover-color;
-    }
-  }
-  .unstyled-link {
-    color: inherit;
-  }
 
   .badge {
     background: @badge-bg;

+ 18 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less

@@ -80,7 +80,7 @@
 
 .dropdown-list-default {
   line-height: 1;
-  border-radius: 2px;
+  border-radius: @dropdown-border-radius;
   font-size: 14px;
   min-width: @dropdown-min-width;
   background: #FFF;
@@ -140,6 +140,23 @@
   }
 }
 
+.grey {
+  color: @grey-color;
+}
+
+.collapsed-form-control {
+  width: 0;
+  padding: 0;
+}
+
+.inherited-color {
+  color: inherit;
+
+  &:hover {
+    color: inherit;
+  }
+}
+
 /**
  * Caret mixin definition.
  * The .caret mixin has two parameters: the width of the caret and the direction of the caret

+ 2 - 2
ambari-logsearch/ambari-logsearch-web/src/app/components/pagination/pagination.component.html

@@ -16,8 +16,8 @@
 -->
 
 <form class="pagination-form" [formGroup]="filtersForm">
-  <filter-dropdown [label]="filterInstance.label" formControlName="pageSize" [options]="filterInstance.options"
-                   [isRightAlign]="true" [isDropup]="true"></filter-dropdown>
+  <filter-dropdown label="{{filterInstance.label | translate}}" formControlName="pageSize"
+                   [options]="filterInstance.options" [isRightAlign]="true" [isDropup]="true"></filter-dropdown>
   <span>{{'pagination.numbers' | translate: numbersTranslateParams}}</span>
   <pagination-controls formControlName="page" [totalCount]="totalCount" [pagesCount]="pagesCount"
                        (currentPageChange)="setCurrentPage($event)"></pagination-controls>

+ 16 - 8
ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.html

@@ -16,17 +16,25 @@
 -->
 
 <label class="parameter-label" *ngFor="let parameter of parameters">
-  <span *ngIf="parameter.isExclude" class="fa fa-search-minus exclude-icon"></span>
+  <span *ngIf="parameter.isExclude" class="fa fa-search-minus"></span>
   {{parameter.label | translate}}:
   <span class="parameter-value">{{parameter.value}}</span>
   <span class="fa fa-times remove-parameter" (click)="removeParameter($event, parameter.id)"></span>
 </label>
 <span class="active-parameter-label" *ngIf="isActive && activeItem">{{activeItem.name | translate}}:</span>
 <div [ngClass]="{'search-item-container': true, 'active': isActive, 'value': isValueInput}">
-  <input #parameterInput auto-complete [(ngModel)]="currentValue" [source]="items" [list-formatter]="itemsListFormatter"
-         display-property-name="name" (valueChanged)="changeParameterName({item: $event, isExclude: false})"
-         class="search-item-input parameter-input form-control">
-  <input #valueInput type="text" [(ngModel)]="currentValue" class="search-item-input value-input form-control"
-         (keyup)="onParameterValueChange($event)">
-  <div class="search-item-text" [innerHTML]="currentValue"></div>
-</div>
+  <span class="parameter-input-wrapper">
+    <input #parameterInput auto-complete class="search-item-input parameter-input form-control"
+           [(ngModel)]="currentValue" [source]="items" display-property-name="name"
+           [list-formatter]="itemsListFormatter" [value-formatter]="itemsValueFormatter" [match-formatted]="true"
+           (valueChanged)="changeParameterName({value: $event.value, isExclude: false})"
+           (keyup)="onParameterKeyUp($event)">
+  </span>
+  <span [ngClass]="{'no-options': !activeItemValueOptions.length}">
+    <input #valueInput auto-complete [(ngModel)]="currentValue" [source]="activeItemValueOptions"
+           [list-formatter]="itemsListFormatter" [value-formatter]="itemsValueFormatter" [match-formatted]="true"
+           (valueChanged)="onParameterValueChange($event.value)" (keydown)="onParameterValueKeyDown($event)"
+           (keyup)="onParameterValueKeyUp($event)" class="search-item-input value-input form-control">
+  </span>
+  <div class="search-item-text">{{currentValue}}</div>
+</div>

+ 14 - 13
ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.less

@@ -20,10 +20,6 @@
 
 @inactive-input-width: 1px;
 @label-margin: 2px;
-.collapsed-form-control {
-  width: 0;
-  padding: 0;
-}
 
 :host {
   display: flex;
@@ -35,16 +31,13 @@
   cursor: text;
 
   .parameter-label {
-    // TODO implement actual styles
     margin: @label-margin;
-    padding: @label-margin;
-    background-color: @main-background-color;
+    border-radius: @dropdown-border-radius;
+    padding: @search-parameter-padding;
+    background-color: @grey-color;
+    color: @base-font-color;
     font-size: 0.8em;
 
-    .exclude-icon {
-      color: @exclude-color;
-    }
-
     .parameter-value {
       font-weight: normal;
     }
@@ -94,14 +87,22 @@
       }
 
       &.value {
-        /deep/ .ng2-auto-complete-wrapper, .parameter-input {
-          display: none;
+        .parameter-input-wrapper {
+          /deep/ .ng2-auto-complete-wrapper {
+            display: none;
+          }
         }
 
         .value-input {
           width: 100%;
         }
       }
+
+      .no-options {
+        /deep/ .ng2-auto-complete {
+          display: none;
+        }
+      }
     }
 
     /deep/ .ng2-auto-complete {

+ 113 - 39
ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts

@@ -19,6 +19,7 @@
 import {Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, forwardRef} from '@angular/core';
 import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
 import {Subject} from 'rxjs/Subject';
+import {SearchBoxParameter, SearchBoxParameterProcessed, SearchBoxParameterTriggered} from '@app/classes/filtering';
 import {CommonEntry} from '@app/classes/models/common-entry';
 import {UtilsService} from '@app/services/utils.service';
 
@@ -42,7 +43,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
     this.rootElement.addEventListener('keydown', this.onRootKeyDown);
   }
 
-  ngOnInit() {
+  ngOnInit(): void {
     this.parameterInput = this.parameterInputRef.nativeElement;
     this.valueInput = this.valueInputRef.nativeElement;
     this.parameterInput.addEventListener('focus', this.onParameterInputFocus);
@@ -50,9 +51,10 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
     this.valueInput.addEventListener('blur', this.onValueInputBlur);
     this.parameterNameChangeSubject.subscribe(this.onParameterNameChange);
     this.parameterAddSubject.subscribe(this.onParameterAdd);
+    this.updateValueSubject.subscribe(this.updateValue);
   }
 
-  ngOnDestroy() {
+  ngOnDestroy(): void {
     this.rootElement.removeEventListener('click', this.onRootClick);
     this.rootElement.removeEventListener('keydown', this.onRootKeyDown);
     this.parameterInput.removeEventListener('focus', this.onParameterInputFocus);
@@ -60,14 +62,15 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
     this.valueInput.removeEventListener('blur', this.onValueInputBlur);
     this.parameterNameChangeSubject.unsubscribe();
     this.parameterAddSubject.unsubscribe();
+    this.updateValueSubject.unsubscribe();
   }
 
+  private readonly messageParameterName: string = 'log_message';
+
   private currentId: number = 0;
 
   private isExclude: boolean = false;
 
-  private defaultSubject: Subject<any> = new Subject();
-
   isActive: boolean = false;
 
   isParameterInput: boolean = false;
@@ -80,10 +83,23 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
   items: CommonEntry[] = [];
 
   @Input()
-  parameterNameChangeSubject: Subject<any> = this.defaultSubject;
+  itemsOptions: {[key: string]: CommonEntry[]};
+
+  @Input()
+  parameterNameChangeSubject: Subject<SearchBoxParameterTriggered> = new Subject();
+
+  @Input()
+  parameterAddSubject: Subject<SearchBoxParameter> = new Subject();
+
+  @Input()
+  updateValueSubject: Subject<void> = new Subject();
 
+  /**
+   * Indicates whether form should receive updated value immediately after user adds new search parameter
+   * @type {boolean}
+   */
   @Input()
-  parameterAddSubject: Subject<any> = this.defaultSubject;
+  updateValueImmediately: boolean = true;
 
   @ViewChild('parameterInput')
   parameterInputRef: ElementRef;
@@ -93,13 +109,18 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
 
   private rootElement: HTMLElement;
 
-  private parameterInput: HTMLElement;
+  private parameterInput: HTMLInputElement;
 
-  private valueInput: HTMLElement;
+  private valueInput: HTMLInputElement;
 
-  activeItem?: any;
+  activeItem: CommonEntry | null = null;
 
-  parameters: any[] = [];
+  parameters: SearchBoxParameterProcessed[] = [];
+
+  get activeItemValueOptions(): CommonEntry[] {
+    return this.itemsOptions && this.activeItem && this.itemsOptions[this.activeItem.value] ?
+      this.itemsOptions[this.activeItem.value] : [];
+  }
 
   private onChange: (fn: any) => void;
 
@@ -133,52 +154,82 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
     }
   };
 
-  private getItem(name: string): CommonEntry {
-    return this.items.find(field => field.value === name);
+  private switchToParameterInput = (): void => {
+    this.activeItem = null;
+    this.isValueInput = false;
+    setTimeout(() => this.parameterInput.focus(), 0);
+  };
+
+  private getItemByValue(name: string): CommonEntry {
+    return this.items.find((field: CommonEntry): boolean => field.value === name);
+  }
+
+  private getItemByName(name: string): CommonEntry {
+    return this.items.find((field: CommonEntry): boolean => field.name === name);
   }
 
   clear(): void {
     this.isActive = false;
     this.activeItem = null;
-    this.currentValue = null;
+    this.currentValue = '';
+    this.parameterInput.value = '';
+    this.valueInput.value = '';
   }
 
   itemsListFormatter(item: CommonEntry): string {
     return item.name;
   }
 
-  changeParameterName(item: any): void {
-    this.parameterNameChangeSubject.next(item);
+  itemsValueFormatter(item: CommonEntry): string {
+    return item.value;
   }
 
-  onParameterNameChange = (options: any): void => {
-    this.activeItem = typeof options.item === 'string' ? this.getItem(options.item) : options.item;
-    this.isExclude = options.isExclude;
-    this.isActive = true;
-    this.isParameterInput = false;
-    this.isValueInput = true;
-    this.currentValue = '';
-    setTimeout(() => this.valueInput.focus(), 0);
+  changeParameterName(options: SearchBoxParameterTriggered): void {
+    this.parameterNameChangeSubject.next(options);
+  }
+
+  onParameterNameChange = (options: SearchBoxParameterTriggered): void => {
+    if (options.value) {
+      this.activeItem = this.getItemByValue(options.value);
+      this.isExclude = options.isExclude;
+      this.isActive = true;
+      this.isParameterInput = false;
+      this.isValueInput = true;
+      this.currentValue = '';
+      setTimeout(() => this.valueInput.focus(), 0);
+    }
   };
 
-  onParameterValueChange(event: KeyboardEvent): void {
+  onParameterValueKeyDown(event: KeyboardEvent): void {
+    if (this.utils.isBackSpacePressed(event) && !this.currentValue) {
+      this.switchToParameterInput();
+    }
+  }
+
+  onParameterValueKeyUp(event: KeyboardEvent): void {
     if (this.utils.isEnterPressed(event) && this.currentValue) {
+      this.onParameterValueChange(this.currentValue);
+    }
+  }
+
+  onParameterValueChange(value: string): void {
+    if (value) {
       this.parameters.push({
         id: this.currentId++,
         name: this.activeItem.value,
         label: this.activeItem.name,
-        value: this.currentValue,
+        value: value,
         isExclude: this.isExclude
       });
-      this.currentValue = '';
-      this.activeItem = null;
-      this.isValueInput = false;
-      this.updateValue();
+      if (this.updateValueImmediately) {
+        this.updateValueSubject.next();
+      }
     }
+    this.switchToParameterInput();
   }
 
-  onParameterAdd = (options: any): void => {
-    const item = this.getItem(options.name);
+  onParameterAdd = (options: SearchBoxParameter): void => {
+    const item = this.getItemByValue(options.name);
     this.parameters.push({
       id: this.currentId++,
       name: options.name,
@@ -186,31 +237,54 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
       value: options.value,
       isExclude: options.isExclude
     });
-    this.updateValue();
+    if (this.updateValueImmediately) {
+      this.updateValueSubject.next();
+    }
+  };
+
+  onParameterKeyUp = (event: KeyboardEvent): void => {
+    if (this.utils.isEnterPressed(event) && this.currentValue) {
+      const existingItem = this.getItemByName(this.currentValue);
+      if (existingItem) {
+        this.changeParameterName({
+          value: this.currentValue,
+          isExclude: false
+        });
+      } else {
+        this.parameterAddSubject.next({
+          name: this.messageParameterName,
+          value: this.currentValue,
+          isExclude: false
+        });
+      }
+    }
   };
 
   removeParameter(event: MouseEvent, id: number): void {
-    this.parameters = this.parameters.filter(parameter => parameter.id !== id);
-    this.updateValue();
+    this.parameters = this.parameters.filter((parameter: SearchBoxParameterProcessed): boolean => parameter.id !== id);
+    if (this.updateValueImmediately) {
+      this.updateValueSubject.next();
+    }
     event.stopPropagation();
   }
 
-  updateValue() {
+  updateValue = (): void => {
+    this.currentValue = '';
     if (this.onChange) {
       this.onChange(this.parameters);
     }
-  }
+  };
 
-  writeValue(parameters: any [] = []) {
+  writeValue(parameters: SearchBoxParameterProcessed[] = []): void {
     this.parameters = parameters;
-    this.updateValue();
+    this.updateValueSubject.next();
   }
 
   registerOnChange(callback: any): void {
     this.onChange = callback;
   }
 
-  registerOnTouched() {
+  registerOnTouched(): void {
   }
 
 }

+ 4 - 3
ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html

@@ -15,12 +15,13 @@
   limitations under the License.
 -->
 
-<dropdown-button class="pull-right" label="logs.columns" [options]="columns" [isRightAlign]="true"
+<dropdown-button class="pull-right" label="{{'logs.columns' | translate}}" [options]="columns" [isRightAlign]="true"
                  [isMultipleChoice]="true" action="updateSelectedColumns"
                  [additionalArgs]="logsTypeMapObject.fieldsModel"></dropdown-button>
 <form *ngIf="logs && logs.length" [formGroup]="filtersForm" class="row pull-right">
-  <filter-dropdown class="col-md-12" [label]="filters.serviceLogsSorting.label" formControlName="serviceLogsSorting"
-                   [options]="filters.serviceLogsSorting.options" [isRightAlign]="true"></filter-dropdown>
+  <filter-dropdown class="col-md-12" label="{{filters.serviceLogsSorting.label | translate}}"
+                   formControlName="serviceLogsSorting" [options]="filters.serviceLogsSorting.options"
+                   [isRightAlign]="true"></filter-dropdown>
 </form>
 <div class="panel panel-default">
   <div class="panel-body">

+ 0 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less

@@ -53,7 +53,6 @@
       }
     }
     &.log-time {
-      color: @grey-color;
       min-width: 7em;
       text-align: right;
     }

+ 6 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.html

@@ -16,8 +16,13 @@
 -->
 
 <div class="pull-right">
+  <form [formGroup]="filtersForm" class="filters">
+    <filter-dropdown *ngIf="isClustersFilterDisplayed" formControlName="clusters" [options]="filters.clusters.options"
+                     [isMultipleChoice]="true" label="{{filters.clusters.label | translate}}" [isRightAlign]="true"
+                     buttonClass="inherited-color"></filter-dropdown>
+  </form>
   <menu-button *ngFor="let item of items" label="{{item.label | translate}}" [action]="item.action"
                [iconClass]="item.iconClass" [labelClass]="item.labelClass" [subItems]="item.subItems"
-               [hideCaret]="item.hideCaret" [badge]="item.badge"  [isRightAlign]="item.isRightAlign">
+               [hideCaret]="item.hideCaret" [badge]="item.badge" [isRightAlign]="item.isRightAlign">
   </menu-button>
 </div>

+ 3 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.less

@@ -20,4 +20,7 @@
 :host {
   .default-flex;
   margin-right: 0;
+  .filters {
+    display: inline-block;
+  }
 }

+ 63 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts

@@ -18,18 +18,80 @@
 
 import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
 import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ReactiveFormsModule} from '@angular/forms';
+import {StoreModule} from '@ngrx/store';
 import {TranslationModules} from '@app/test-config.spec';
+import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service';
+import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service';
+import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service';
+import {ServiceLogsFieldsService, serviceLogsFields} from '@app/services/storage/service-logs-fields.service';
+import {
+  ServiceLogsHistogramDataService, serviceLogsHistogramData
+} from '@app/services/storage/service-logs-histogram-data.service';
+import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service';
+import {AppStateService, appState} from '@app/services/storage/app-state.service';
+import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
+import {TabsService, tabs} from '@app/services/storage/tabs.service';
+import {ClustersService, clusters} from '@app/services/storage/clusters.service';
+import {ComponentsService, components} from '@app/services/storage/components.service';
+import {HostsService, hosts} from '@app/services/storage/hosts.service';
+import {LogsContainerService} from '@app/services/logs-container.service';
+import {HttpClientService} from '@app/services/http-client.service';
 
 import {TopMenuComponent} from './top-menu.component';
 
 describe('TopMenuComponent', () => {
   let component: TopMenuComponent;
   let fixture: ComponentFixture<TopMenuComponent>;
+  const httpClient = {
+    get: () => {
+      return {
+        subscribe: () => {
+        }
+      };
+    }
+  };
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      imports: TranslationModules,
+      imports: [
+        ReactiveFormsModule,
+        StoreModule.provideStore({
+          auditLogs,
+          serviceLogs,
+          auditLogsFields,
+          serviceLogsFields,
+          serviceLogsHistogramData,
+          serviceLogsTruncated,
+          appState,
+          appSettings,
+          tabs,
+          clusters,
+          components,
+          hosts
+        }),
+        ...TranslationModules
+      ],
       declarations: [TopMenuComponent],
+      providers: [
+        LogsContainerService,
+        {
+          provide: HttpClientService,
+          useValue: httpClient
+        },
+        AuditLogsService,
+        ServiceLogsService,
+        AuditLogsFieldsService,
+        ServiceLogsFieldsService,
+        ServiceLogsHistogramDataService,
+        ServiceLogsTruncatedService,
+        AppStateService,
+        AppSettingsService,
+        TabsService,
+        ClustersService,
+        ComponentsService,
+        HostsService
+      ],
       schemas: [CUSTOM_ELEMENTS_SCHEMA]
     })
     .compileComponents();

+ 24 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts

@@ -17,6 +17,10 @@
  */
 
 import {Component} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+import {FilterCondition, TimeUnitListItem} from '@app/classes/filtering';
+import {ListItem} from '@app/classes/list-item';
+import {LogsContainerService} from '@app/services/logs-container.service';
 
 @Component({
   selector: 'top-menu',
@@ -25,10 +29,21 @@ import {Component} from '@angular/core';
 })
 export class TopMenuComponent {
 
+  constructor(private logsContainer: LogsContainerService) {
+  }
+
+  get filtersForm(): FormGroup {
+    return this.logsContainer.filtersForm;
+  };
+
+  get filters(): {[key: string]: FilterCondition} {
+    return this.logsContainer.filters;
+  };
+
   //TODO implement loading of real data into subItems
   readonly items = [
     {
-      iconClass: 'fa fa-user unstyled-link',
+      iconClass: 'fa fa-user grey',
       hideCaret: true,
       isRightAlign: true,
       subItems: [
@@ -43,4 +58,12 @@ export class TopMenuComponent {
     }
   ];
 
+  get clusters(): (ListItem | TimeUnitListItem[])[] {
+    return this.filters.clusters.options;
+  }
+
+  get isClustersFilterDisplayed(): boolean {
+    return this.logsContainer.isFilterConditionDisplayed('clusters') && this.clusters.length > 1;
+  }
+
 }

+ 4 - 3
ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less

@@ -23,12 +23,11 @@
 @button-border-radius: 4px;
 @input-border-width: 1px;
 @input-border: @input-border-width solid #CFD3D7;
-@button-border-radius: 4px;
 @input-group-addon-padding: 6px 12px 6px 0;
 @block-margin-top: 20px;
 @link-color: #1491C1;
 @link-hover-color: #23527C;
-@grey-color: #666;
+@grey-color: #DDD;
 @default-line-height: 1.42857143;
 @main-background-color: #ECECEC;
 @filters-panel-background-color: #FFF;
@@ -37,9 +36,11 @@
 @checkbox-top: 4px;
 @dropdown-min-width: 160px;
 @dropdown-max-height: 500px; // TODO get rid of magic number, base on actual design
+@dropdown-border-radius: 2px;
 @input-height: 34px;
 @input-padding: 10px;
 @col-padding: 15px;
+@search-parameter-padding: 5px 2px;
 
 @fatal-color: #830A0A;
 @error-color: #E81D1D;
@@ -63,4 +64,4 @@
 @icon-padding: 5px;
 
 // Table
-@table-border-color: #EEEEEE;
+@table-border-color: #EEE;

+ 3 - 1
ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.spec.ts

@@ -17,6 +17,7 @@
  */
 
 import {TestBed, inject} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
 import {StoreModule} from '@ngrx/store';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {AppStateService, appState} from '@app/services/storage/app-state.service';
@@ -62,7 +63,8 @@ describe('ComponentActionsService', () => {
           serviceLogsHistogramData,
           serviceLogsTruncated,
           tabs
-        })
+        }),
+        ...TranslationModules
       ],
       providers: [
         ComponentActionsService,

+ 1 - 1
ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.ts

@@ -132,7 +132,7 @@ export class ComponentActionsService {
   }
 
   proceedWithExclude = (item: string): void => this.logsContainer.queryParameterNameChange.next({
-    item: item,
+    value: item,
     isExclude: true
   });
 

+ 3 - 1
ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts

@@ -17,6 +17,7 @@
  */
 
 import {TestBed, inject} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
 import {StoreModule} from '@ngrx/store';
 import {HostsService, hosts} from '@app/services/storage/hosts.service';
 import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service';
@@ -60,7 +61,8 @@ describe('ComponentGeneratorService', () => {
           components,
           serviceLogsTruncated,
           tabs
-        })
+        }),
+        ...TranslationModules
       ],
       providers: [
         ComponentGeneratorService,

+ 3 - 1
ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts

@@ -17,6 +17,7 @@
  */
 
 import {TestBed, inject} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
 import {StoreModule} from '@ngrx/store';
 import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service';
 import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service';
@@ -61,7 +62,8 @@ describe('LogsContainerService', () => {
           hosts,
           serviceLogsTruncated,
           tabs
-        })
+        }),
+        ...TranslationModules
       ],
       providers: [
         AuditLogsService,

+ 58 - 22
ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts

@@ -27,6 +27,7 @@ import 'rxjs/add/operator/first';
 import 'rxjs/add/operator/map';
 import 'rxjs/add/operator/takeUntil';
 import * as moment from 'moment-timezone';
+import {TranslateService} from '@ngx-translate/core';
 import {HttpClientService} from '@app/services/http-client.service';
 import {AuditLogsService} from '@app/services/storage/audit-logs.service';
 import {AuditLogsFieldsService} from '@app/services/storage/audit-logs-fields.service';
@@ -41,8 +42,12 @@ import {ClustersService} from '@app/services/storage/clusters.service';
 import {ComponentsService} from '@app/services/storage/components.service';
 import {HostsService} from '@app/services/storage/hosts.service';
 import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry';
-import {FilterCondition, TimeUnitListItem, SortingListItem} from '@app/classes/filtering';
+import {
+  FilterCondition, TimeUnitListItem, SortingListItem, SearchBoxParameter, SearchBoxParameterTriggered
+} from '@app/classes/filtering';
 import {ListItem} from '@app/classes/list-item';
+import {LogsType, ScrollType, SortingType} from '@app/classes/string';
+
 import {Tab} from '@app/classes/models/tab';
 import {LogField} from '@app/classes/models/log-field';
 import {AuditLog} from '@app/classes/models/audit-log';
@@ -51,14 +56,15 @@ import {ServiceLog} from '@app/classes/models/service-log';
 import {ServiceLogField} from '@app/classes/models/service-log-field';
 import {BarGraph} from '@app/classes/models/bar-graph';
 import {NodeItem} from '@app/classes/models/node-item';
+import {CommonEntry} from '@app/classes/models/common-entry';
 
 @Injectable()
 export class LogsContainerService {
 
   constructor(
-    private httpClient: HttpClientService, private auditLogsStorage: AuditLogsService,
-    private auditLogsFieldsStorage: AuditLogsFieldsService, private serviceLogsStorage: ServiceLogsService,
-    private serviceLogsFieldsStorage: ServiceLogsFieldsService,
+    private translate: TranslateService, private httpClient: HttpClientService,
+    private auditLogsStorage: AuditLogsService, private auditLogsFieldsStorage: AuditLogsFieldsService,
+    private serviceLogsStorage: ServiceLogsService, private serviceLogsFieldsStorage: ServiceLogsFieldsService,
     private serviceLogsHistogramStorage: ServiceLogsHistogramDataService,
     private serviceLogsTruncatedStorage: ServiceLogsTruncatedService, private appState: AppStateService,
     private appSettings: AppSettingsService, private tabsStorage: TabsService, private clustersStorage: ClustersService,
@@ -78,7 +84,7 @@ export class LogsContainerService {
     this.loadHosts();
     appState.getParameter('activeLog').subscribe((value: ActiveServiceLogEntry | null) => this.activeLog = value);
     appState.getParameter('isServiceLogsFileView').subscribe((value: boolean) => this.isServiceLogsFileView = value);
-    appState.getParameter('activeLogsType').subscribe((value: string) => this.activeLogsType = value);
+    appState.getParameter('activeLogsType').subscribe((value: LogsType) => this.activeLogsType = value);
     appSettings.getParameter('timeZone').subscribe((value: string) => this.timeZone = value || this.defaultTimeZone);
     tabsStorage.mapCollection((tab: Tab): Tab => {
       let currentAppState = tab.appState || {};
@@ -111,6 +117,8 @@ export class LogsContainerService {
         this.loadLogs();
       });
     });
+    this.auditLogsColumns.subscribe(this.getTranslationKeysSubscriber('auditLogsColumnsTranslated'));
+    this.serviceLogsColumns.subscribe(this.getTranslationKeysSubscriber('serviceLogsColumnsTranslated'));
   }
 
   private readonly paginationOptions: string[] = ['10', '25', '50', '100'];
@@ -119,7 +127,8 @@ export class LogsContainerService {
     clusters: {
       label: 'filter.clusters',
       options: [],
-      defaultSelection: []
+      defaultSelection: [],
+      fieldName: 'cluster'
     },
     timeRange: {
       options: [
@@ -346,7 +355,8 @@ export class LogsContainerService {
       label: 'filter.components',
       iconClass: 'fa fa-cubes',
       options: [],
-      defaultSelection: []
+      defaultSelection: [],
+      fieldName: 'type'
     },
     levels: {
       label: 'filter.levels',
@@ -381,13 +391,15 @@ export class LogsContainerService {
           value: 'UNKNOWN'
         }
       ],
-      defaultSelection: []
+      defaultSelection: [],
+      fieldName: 'level'
     },
     hosts: {
       label: 'filter.hosts',
       iconClass: 'fa fa-server',
       options: [],
-      defaultSelection: []
+      defaultSelection: [],
+      fieldName: 'host'
     },
     auditLogsSorting: {
       label: 'sorting.title',
@@ -533,9 +545,9 @@ export class LogsContainerService {
 
   activeLog: ActiveServiceLogEntry | null = null;
 
-  activeLogsType: string;
+  activeLogsType: LogsType;
 
-  private filtersFormChange: Subject<any> = new Subject();
+  private filtersFormChange: Subject<void> = new Subject();
 
   private columnsMapper<FieldT extends LogField>(fields: FieldT[]): ListItem[] {
     return fields.filter((field: FieldT): boolean => field.isAvailable).map((field: FieldT): ListItem => {
@@ -561,10 +573,30 @@ export class LogsContainerService {
     }
   }
 
+  private getTranslationKeysSubscriber = (propertyName: string): (items: ListItem[]) => void  => {
+    return (items: ListItem[]): void => {
+      const keys = items.map((item: ListItem): string => item.label);
+      if (keys.length) {
+        this.translate.get(keys).first().subscribe((translation: {[key: string]: string}): void => {
+          this[propertyName] = items.map((item: ListItem): CommonEntry => {
+            return {
+              name: translation[item.label],
+              value: item.value
+            };
+          });
+        });
+      }
+    };
+  };
+
   auditLogsColumns: Observable<ListItem[]> = this.auditLogsFieldsStorage.getAll().map(this.columnsMapper);
 
+  auditLogsColumnsTranslated: CommonEntry[] = [];
+
   serviceLogsColumns: Observable<ListItem[]> = this.serviceLogsFieldsStorage.getAll().map(this.columnsMapper);
 
+  serviceLogsColumnsTranslated: CommonEntry[] = [];
+
   serviceLogs: Observable<ServiceLog[]> = Observable.combineLatest(this.serviceLogsStorage.getAll(), this.serviceLogsColumns).map(this.logsMapper);
 
   auditLogs: Observable<AuditLog[]> = Observable.combineLatest(this.auditLogsStorage.getAll(), this.auditLogsColumns).map(this.logsMapper);
@@ -593,13 +625,13 @@ export class LogsContainerService {
     };
   }
 
-  queryParameterNameChange: Subject<any> = new Subject();
+  queryParameterNameChange: Subject<SearchBoxParameterTriggered> = new Subject();
 
-  queryParameterAdd: Subject<any> = new Subject();
+  queryParameterAdd: Subject<SearchBoxParameter> = new Subject();
 
-  private stopTimer: Subject<any> = new Subject();
+  private stopTimer: Subject<void> = new Subject();
 
-  private stopAutoRefreshCountdown: Subject<any> = new Subject();
+  private stopAutoRefreshCountdown: Subject<void> = new Subject();
 
   captureSeconds: number = 0;
 
@@ -611,7 +643,7 @@ export class LogsContainerService {
 
   private stopCaptureTime: number;
 
-  loadLogs = (logsType: string = this.activeLogsType): void => {
+  loadLogs = (logsType: LogsType = this.activeLogsType): void => {
     this.httpClient.get(logsType, this.getParams('listFilters')).subscribe((response: Response): void => {
       const jsonResponse = response.json(),
         model = this.logsTypeMap[logsType].logsModel;
@@ -640,7 +672,7 @@ export class LogsContainerService {
     }
   };
 
-  loadLogContext(id: string, hostName: string, componentName: string, scrollType: 'before' | 'after' | '' = ''): void {
+  loadLogContext(id: string, hostName: string, componentName: string, scrollType: ScrollType = ''): void {
     const params = {
       id: id,
       host_name: hostName,
@@ -671,7 +703,7 @@ export class LogsContainerService {
     });
   }
 
-  private getParams(filtersMapName: string, logsType: string = this.activeLogsType): {[key: string]: string} {
+  private getParams(filtersMapName: string, logsType: LogsType = this.activeLogsType): {[key: string]: string} {
     let params = {};
     this.logsTypeMap[logsType][filtersMapName].forEach((key: string): void => {
       const inputValue = this.filtersForm.getRawValue()[key],
@@ -787,11 +819,11 @@ export class LogsContainerService {
     return endMoment ? endMoment.toISOString() : '';
   };
 
-  private getQuery(isExclude: boolean): (value: any[]) => string {
-    return (value: any[]): string => {
+  private getQuery(isExclude: boolean): (value: SearchBoxParameter[]) => string {
+    return (value: SearchBoxParameter[]): string => {
       let parameters;
       if (value && value.length) {
-        parameters = value.filter(item => item.isExclude === isExclude).map(parameter => {
+        parameters = value.filter((item: SearchBoxParameter): boolean => item.isExclude === isExclude).map((parameter: SearchBoxParameter): {[key: string]: string} => {
           return {
             [parameter.name]: parameter.value.replace(/\s/g, '+')
           };
@@ -801,7 +833,7 @@ export class LogsContainerService {
     }
   }
 
-  private getSortType(selection: SortingListItem[] = []): 'asc' | 'desc' {
+  private getSortType(selection: SortingListItem[] = []): SortingType {
     return selection[0] && selection[0].value ? selection[0].value.type : 'desc';
   }
 
@@ -931,4 +963,8 @@ export class LogsContainerService {
     }, {});
   }
 
+  isFilterConditionDisplayed(key: string): boolean {
+    return this.logsTypeMap[this.activeLogsType].listFilters.indexOf(key) > -1
+  }
+
 }

+ 4 - 0
ambari-logsearch/ambari-logsearch-web/src/app/services/utils.service.ts

@@ -77,6 +77,10 @@ export class UtilsService {
     return event.keyCode === 13;
   }
 
+  isBackSpacePressed(event: KeyboardEvent): boolean {
+    return event.keyCode === 8;
+  }
+
   isDifferentDates(dateA, dateB, timeZone): boolean {
     const momentA = moment(dateA).tz(timeZone),
       momentB = moment(dateB).tz(timeZone);

+ 1 - 1
ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json

@@ -26,7 +26,7 @@
   "filter.clusters": "Clusters",
   "filter.components": "Components",
   "filter.levels": "Levels",
-  "filter.excluded": "Excluded",
+  "filter.exclude": "Exclude",
   "filter.hosts": "Hosts",
 
   "filter.capture": "Capture",

+ 10 - 10
ambari-logsearch/ambari-logsearch-web/webpack.config.js

@@ -102,11 +102,11 @@ module.exports = {
       "./src/polyfills.ts"
     ],
     "styles": [
-      "./src/styles.less",
       "./node_modules/bootstrap/dist/css/bootstrap.min.css",
       "./node_modules/font-awesome/css/font-awesome.min.css",
       "./src/vendor/css/bootstrap-logsearch.min.css",
-      "./src/vendor/css/bootstrap-datetimepicker.min.css"
+      "./src/vendor/css/bootstrap-datetimepicker.min.css",
+      "./src/styles.less"
     ]
   },
   "output": {
@@ -271,11 +271,11 @@ module.exports = {
       },
       {
         "include": [
-          path.join(process.cwd(), "src/styles.less"),
           path.join(process.cwd(), "node_modules/bootstrap/dist/css/bootstrap.min.css"),
           path.join(process.cwd(), "node_modules/font-awesome/css/font-awesome.min.css"),
           path.join(process.cwd(), "src/vendor/css/bootstrap-logsearch.min.css"),
-          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css")
+          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css"),
+          path.join(process.cwd(), "src/styles.less")
         ],
         "test": /\.css$/,
         "use": [
@@ -298,11 +298,11 @@ module.exports = {
       },
       {
         "include": [
-          path.join(process.cwd(), "src/styles.less"),
           path.join(process.cwd(), "node_modules/bootstrap/dist/css/bootstrap.min.css"),
           path.join(process.cwd(), "node_modules/font-awesome/css/font-awesome.min.css"),
           path.join(process.cwd(), "src/vendor/css/bootstrap-logsearch.min.css"),
-          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css")
+          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css"),
+          path.join(process.cwd(), "src/styles.less")
         ],
         "test": /\.scss$|\.sass$/,
         "use": [
@@ -333,11 +333,11 @@ module.exports = {
       },
       {
         "include": [
-          path.join(process.cwd(), "src/styles.less"),
           path.join(process.cwd(), "node_modules/bootstrap/dist/css/bootstrap.min.css"),
           path.join(process.cwd(), "node_modules/font-awesome/css/font-awesome.min.css"),
           path.join(process.cwd(), "src/vendor/css/bootstrap-logsearch.min.css"),
-          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css")
+          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css"),
+          path.join(process.cwd(), "src/styles.less")
         ],
         "test": /\.less$/,
         "use": [
@@ -367,11 +367,11 @@ module.exports = {
       },
       {
         "include": [
-          path.join(process.cwd(), "src/styles.less"),
           path.join(process.cwd(), "node_modules/bootstrap/dist/css/bootstrap.min.css"),
           path.join(process.cwd(), "node_modules/font-awesome/css/font-awesome.min.css"),
           path.join(process.cwd(), "src/vendor/css/bootstrap-logsearch.min.css"),
-          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css")
+          path.join(process.cwd(), "src/vendor/css/bootstrap-datetimepicker.min.css"),
+          path.join(process.cwd(), "src/styles.less")
         ],
         "test": /\.styl$/,
         "use": [

+ 1 - 1
ambari-logsearch/docker/test-config/logfeeder/logfeeder.properties

@@ -29,4 +29,4 @@ logfeeder.cache.key.field=log_message
 logfeeder.cache.dedup.interval=1000
 logfeeder.cache.last.dedup.enabled=true
 logsearch.config.zk_connect_string=localhost:9983
-logfeeder.include.default.level=FATAL,ERROR,WARN,INFO,DEBUG,TRACE,UNKNOWN
+logfeeder.include.default.level=FATAL,ERROR,WARN,INFO,DEBUG,TRACE,UNKNOWN

+ 2 - 3
ambari-metrics/ambari-metrics-hadoop-sink/pom.xml

@@ -31,7 +31,7 @@ limitations under the License.
   <packaging>jar</packaging>
   <properties>
     <sinkJarName>${project.artifactId}-with-common-${project.version}.jar</sinkJarName>
-    <hadoop.version>3.0.0-beta1</hadoop.version>
+    <hadoopVersion>3.0.0-beta1</hadoopVersion>
   </properties>
 
 
@@ -116,7 +116,6 @@ limitations under the License.
           </execution>
         </executions>
         <configuration>
-          <skip>true</skip>
           <attach>false</attach>
           <submodules>false</submodules>
           <controlDir>${project.basedir}/../src/main/package/deb/control</controlDir>
@@ -142,7 +141,7 @@ limitations under the License.
     <dependency>
       <groupId>org.apache.hadoop</groupId>
       <artifactId>hadoop-common</artifactId>
-      <version>${hadoop.version}</version>
+      <version>${hadoopVersion}</version>
       <scope>compile</scope>
     </dependency>
     <dependency>

+ 5 - 0
ambari-server/src/main/java/org/apache/ambari/server/actionmanager/ExecutionCommandWrapper.java

@@ -299,7 +299,12 @@ public class ExecutionCommandWrapper {
       if (null != repositoryVersion) {
         // only set the version if it's not set and this is NOT an install
         // command
+        // Some stack scripts use version for path purposes.  Sending unresolved version first (for
+        // blueprints) and then resolved one would result in various issues: duplicate directories
+        // (/hdp/apps/2.6.3.0 + /hdp/apps/2.6.3.0-235), parent directory not found, and file not
+        // found, etc.  Hence requiring repositoryVersion to be resolved.
         if (!commandParams.containsKey(VERSION)
+          && repositoryVersion.isResolved()
           && executionCommand.getRoleCommand() != RoleCommand.INSTALL) {
           commandParams.put(VERSION, repositoryVersion.getVersion());
         }

+ 7 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/handlers/CreateHandler.java

@@ -24,6 +24,7 @@ import org.apache.ambari.server.api.services.Result;
 import org.apache.ambari.server.api.services.ResultImpl;
 import org.apache.ambari.server.api.services.ResultMetadata;
 import org.apache.ambari.server.api.services.ResultStatus;
+import org.apache.ambari.server.controller.internal.OperationStatusMetaData;
 import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
 import org.apache.ambari.server.controller.spi.RequestStatus;
 import org.apache.ambari.server.controller.spi.RequestStatusMetaData;
@@ -94,6 +95,11 @@ public class CreateHandler extends BaseManagementHandler {
       return null;
     }
 
-    throw new UnsupportedOperationException();
+    if (requestStatusMetaData.getClass() == OperationStatusMetaData.class) {
+      return (OperationStatusMetaData) requestStatusMetaData;
+    } else {
+      throw new IllegalArgumentException(String.format("RequestStatusDetails is of an expected type: %s",
+          requestStatusMetaData.getClass().getName()));
+    }
   }
 }

+ 7 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/handlers/UpdateHandler.java

@@ -24,6 +24,7 @@ import org.apache.ambari.server.api.services.Result;
 import org.apache.ambari.server.api.services.ResultImpl;
 import org.apache.ambari.server.api.services.ResultMetadata;
 import org.apache.ambari.server.api.services.ResultStatus;
+import org.apache.ambari.server.controller.internal.OperationStatusMetaData;
 import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
 import org.apache.ambari.server.controller.spi.NoSuchResourceException;
 import org.apache.ambari.server.controller.spi.RequestStatus;
@@ -79,6 +80,11 @@ public class UpdateHandler extends BaseManagementHandler {
       return null;
     }
 
-    throw new UnsupportedOperationException();
+    if (requestStatusMetaData.getClass() == OperationStatusMetaData.class) {
+      return (OperationStatusMetaData) requestStatusMetaData;
+    } else {
+      throw new IllegalArgumentException(String.format("RequestStatusDetails is of an expected type: %s",
+          requestStatusMetaData.getClass().getName()));
+    }
   }
 }

+ 3 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/resources/ResourceInstanceFactoryImpl.java

@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.Set;
 
 import org.apache.ambari.server.api.query.QueryImpl;
+import org.apache.ambari.server.api.services.RootServiceComponentConfigurationService;
 import org.apache.ambari.server.controller.internal.ClusterKerberosDescriptorResourceProvider;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.ambari.server.controller.utilities.ClusterControllerHelper;
@@ -291,7 +292,8 @@ public class ResourceInstanceFactoryImpl implements ResourceInstanceFactory {
 
       case RootServiceComponentConfiguration:
         resourceDefinition = new SimpleResourceDefinition(Resource.Type.RootServiceComponentConfiguration,
-            "configuration", "configurations");
+            "configuration", "configurations",
+            null, RootServiceComponentConfigurationService.DIRECTIVES_MAP);
         break;
 
       case RootServiceHostComponent:

+ 15 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/services/RootServiceComponentConfigurationService.java

@@ -34,6 +34,7 @@ package org.apache.ambari.server.api.services;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -48,10 +49,14 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 
+import org.apache.ambari.server.api.resources.BaseResourceDefinition;
 import org.apache.ambari.server.api.resources.ResourceInstance;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.http.HttpStatus;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
@@ -91,6 +96,16 @@ public class RootServiceComponentConfigurationService extends BaseService {
   private static final String REQUEST_TYPE =
       "org.apache.ambari.server.api.services.RootServiceComponentConfigurationRequestSwagger";
 
+  public static final String DIRECTIVE_OPERATION = "op";
+
+  private static final Set<String> DIRECTIVES = Sets.newHashSet(DIRECTIVE_OPERATION);
+
+  public static final Map<BaseResourceDefinition.DirectiveType, Set<String>> DIRECTIVES_MAP =
+      ImmutableMap.<BaseResourceDefinition.DirectiveType, Set<String>>builder()
+          .put(BaseResourceDefinition.DirectiveType.CREATE, DIRECTIVES)
+          .put(BaseResourceDefinition.DirectiveType.UPDATE, DIRECTIVES)
+          .build();
+
   private final String serviceName;
   private final String componentName;
 

+ 0 - 46
ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/AmbariConfiguration.java

@@ -1,46 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.ambari.server.api.services.ldap;
-
-import java.util.Map;
-
-/**
- * Domain POJO representing generic ambari configuration data.
- */
-public class AmbariConfiguration {
-
-  /**
-   * The type of the configuration,  eg.: ldap-configuration
-   */
-  private String type;
-
-  private Map<String, String> properties = null;
-
-  public String getType() {
-    return type;
-  }
-
-  public void setType(String type) {
-    this.type = type;
-  }
-
-  public Map<String, String> getProperties() {
-    return properties;
-  }
-
-  public void setProperties(Map<String, String> data) {
-    this.properties = data;
-  }
-}

+ 0 - 43
ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapConfigOperation.java

@@ -1,43 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.ambari.server.api.services.ldap;
-
-/**
- * Enumeration for supported operations related to LDAP configuration.
- */
-public enum LdapConfigOperation {
-  TEST_CONNECTION("test-connection"),
-  TEST_ATTRIBUTES("test-attributes"),
-  DETECT_ATTRIBUTES("detect-attributes");
-
-  private String actionStr;
-
-  LdapConfigOperation(String actionStr) {
-    this.actionStr = actionStr;
-  }
-
-  public static LdapConfigOperation fromAction(String action) {
-    for (LdapConfigOperation val : LdapConfigOperation.values()) {
-      if (val.action().equals(action)) {
-        return val;
-      }
-    }
-    throw new IllegalStateException("Action [ " + action + " ] is not supported");
-  }
-
-  public String action() {
-    return this.actionStr;
-  }
-}

+ 0 - 49
ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapConfigurationRequest.java

@@ -1,49 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.ambari.server.api.services.ldap;
-
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Request object wrapping information for LDAP configuration related request calls.
- */
-public class LdapConfigurationRequest {
-
-  @SerializedName("Configuration")
-  private AmbariConfiguration ambariConfiguration;
-
-  @SerializedName("RequestInfo")
-  private LdapRequestInfo requestInfo;
-
-  public LdapConfigurationRequest() {
-  }
-
-  public AmbariConfiguration getAmbariConfiguration() {
-    return ambariConfiguration;
-  }
-
-  public void setAmbariConfiguration(AmbariConfiguration ambariConfiguration) {
-    this.ambariConfiguration = ambariConfiguration;
-  }
-
-  public LdapRequestInfo getRequestInfo() {
-    return requestInfo;
-  }
-
-  public void setRequestInfo(LdapRequestInfo requestInfo) {
-    this.requestInfo = requestInfo;
-  }
-}

+ 0 - 185
ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapConfigurationService.java

@@ -1,185 +0,0 @@
-/*
- * 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.
- */
-
-/*
- * 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.
- */
-
-package org.apache.ambari.server.api.services.ldap;
-
-import java.util.Set;
-
-import javax.inject.Inject;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-
-import org.apache.ambari.annotations.ApiIgnore;
-import org.apache.ambari.server.StaticallyInject;
-import org.apache.ambari.server.api.services.BaseService;
-import org.apache.ambari.server.api.services.Result;
-import org.apache.ambari.server.api.services.ResultImpl;
-import org.apache.ambari.server.api.services.ResultStatus;
-import org.apache.ambari.server.controller.internal.ResourceImpl;
-import org.apache.ambari.server.controller.spi.Resource;
-import org.apache.ambari.server.ldap.domain.AmbariLdapConfiguration;
-import org.apache.ambari.server.ldap.domain.AmbariLdapConfigurationFactory;
-import org.apache.ambari.server.ldap.service.LdapFacade;
-import org.apache.ambari.server.security.authorization.AuthorizationException;
-import org.apache.ambari.server.security.authorization.AuthorizationHelper;
-import org.apache.ambari.server.security.authorization.ResourceType;
-import org.apache.ambari.server.security.authorization.RoleAuthorization;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.core.Authentication;
-
-import com.google.common.collect.Sets;
-
-/**
- * Endpoint designated to LDAP specific operations.
- */
-@StaticallyInject
-@Path("/ldapconfigs/")
-public class LdapConfigurationService extends BaseService {
-
-  private static final Logger LOGGER = LoggerFactory.getLogger(LdapConfigurationService.class);
-
-  @Inject
-  private static LdapFacade ldapFacade;
-
-  @Inject
-  private static AmbariLdapConfigurationFactory ambariLdapConfigurationFactory;
-
-
-  @POST
-  @ApiIgnore // until documented
-  @Path("/validate")
-  @Consumes(MediaType.APPLICATION_JSON)
-  @Produces(MediaType.APPLICATION_JSON)
-  public Response validateConfiguration(LdapConfigurationRequest ldapConfigurationRequest) {
-
-    // check if the user is authorized to perform the operation
-    authorize();
-
-    Set<String> groups = Sets.newHashSet();
-    Object responseEntity = null;
-
-    Result result = new ResultImpl(new ResultStatus(ResultStatus.STATUS.OK));
-    try {
-
-      validateRequest(ldapConfigurationRequest);
-
-      AmbariLdapConfiguration ambariLdapConfiguration = ambariLdapConfigurationFactory.createLdapConfiguration(
-        ldapConfigurationRequest.getAmbariConfiguration().getProperties());
-
-      LdapConfigOperation action = LdapConfigOperation.fromAction(ldapConfigurationRequest.getRequestInfo().getAction());
-      switch (action) {
-
-        case TEST_CONNECTION:
-
-          LOGGER.info("Testing connection to the LDAP server ...");
-          ldapFacade.checkConnection(ambariLdapConfiguration);
-
-          break;
-        case TEST_ATTRIBUTES:
-
-          LOGGER.info("Testing LDAP attributes ....");
-          groups = ldapFacade.checkLdapAttributes(ldapConfigurationRequest.getRequestInfo().getParameters(), ambariLdapConfiguration);
-          responseEntity = groups;
-
-          break;
-        case DETECT_ATTRIBUTES:
-
-          LOGGER.info("Detecting LDAP attributes ...");
-          ambariLdapConfiguration = ldapFacade.detectAttributes(ambariLdapConfiguration);
-          responseEntity = ambariLdapConfiguration;
-
-          break;
-        default:
-          LOGGER.warn("No action provided ...");
-          throw new IllegalArgumentException("No request action provided");
-      }
-
-    } catch (Exception e) {
-      result.setResultStatus(new ResultStatus(ResultStatus.STATUS.SERVER_ERROR, e));
-      responseEntity = e.getMessage();
-    }
-
-    return Response.status(result.getStatus().getStatusCode()).entity(responseEntity).build();
-  }
-
-  private void setResult(Set<String> groups, Result result) {
-    Resource resource = new ResourceImpl(Resource.Type.RootServiceComponentConfiguration);
-    resource.setProperty("groups", groups);
-    result.getResultTree().addChild(resource, "payload");
-  }
-
-  private void validateRequest(LdapConfigurationRequest ldapConfigurationRequest) {
-    String errMsg;
-
-    if (null == ldapConfigurationRequest) {
-      errMsg = "No ldap configuraiton request provided";
-      LOGGER.error(errMsg);
-      throw new IllegalArgumentException(errMsg);
-    }
-
-    if (null == ldapConfigurationRequest.getRequestInfo()) {
-      errMsg = String.format("No request information provided. Request: [%s]", ldapConfigurationRequest);
-      LOGGER.error(errMsg);
-      throw new IllegalArgumentException(errMsg);
-    }
-
-    if (null == ldapConfigurationRequest.getAmbariConfiguration()
-      || ldapConfigurationRequest.getAmbariConfiguration().getProperties() != null) {
-      errMsg = String.format("No / Invalid configuration data provided. Request: [%s]", ldapConfigurationRequest);
-      LOGGER.error(errMsg);
-      throw new IllegalArgumentException(errMsg);
-    }
-  }
-
-  private void authorize() {
-    try {
-      Authentication authentication = AuthorizationHelper.getAuthentication();
-
-      if (authentication == null || !authentication.isAuthenticated()) {
-        throw new AuthorizationException("Authentication data is not available, authorization to perform the requested operation is not granted");
-      }
-
-      if (!AuthorizationHelper.isAuthorized(authentication, ResourceType.AMBARI, null, requiredAuthorizations())) {
-        throw new AuthorizationException("The authenticated user does not have the appropriate authorizations to create the requested resource(s)");
-      }
-    } catch (AuthorizationException e) {
-      LOGGER.error("Unauthorized operation.", e);
-      throw new IllegalArgumentException("User is not authorized to perform the operation", e);
-    }
-
-  }
-
-  private Set<RoleAuthorization> requiredAuthorizations() {
-    return Sets.newHashSet(RoleAuthorization.AMBARI_MANAGE_CONFIGURATION);
-  }
-}

+ 0 - 61
ambari-server/src/main/java/org/apache/ambari/server/api/services/ldap/LdapRequestInfo.java

@@ -1,61 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.ambari.server.api.services.ldap;
-
-import java.util.Map;
-
-import org.apache.ambari.server.controller.RequestPostRequest;
-
-/**
- * Bean holding LDAP request specific request information.
- */
-public class LdapRequestInfo implements RequestPostRequest.RequestInfo {
-
-  // no-arg costructor facilitating JSON serialization
-  public LdapRequestInfo() {
-  }
-
-  private String action;
-
-  private Map<String, Object> parameters;
-
-  @Override
-  public String getAction() {
-    return action;
-  }
-
-  public void setAction(String action) {
-    this.action = action;
-  }
-
-  public void setParameters(Map<String, Object> parameters) {
-    this.parameters = parameters;
-  }
-
-  @Override
-  public String getCommand() {
-    return null;
-  }
-
-  @Override
-  public RequestPostRequest.OperationLevel getOperationLevel() {
-    return null;
-  }
-
-  @Override
-  public Map<String, Object> getParameters() {
-    return parameters;
-  }
-}

+ 33 - 5
ambari-server/src/main/java/org/apache/ambari/server/api/services/serializers/JsonSerializer.java

@@ -31,6 +31,7 @@ import org.apache.ambari.server.api.services.ResultMetadata;
 import org.apache.ambari.server.api.services.ResultStatus;
 import org.apache.ambari.server.api.util.TreeNode;
 import org.apache.ambari.server.api.util.TreeNodeImpl;
+import org.apache.ambari.server.controller.internal.OperationStatusMetaData;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.codehaus.jackson.JsonFactory;
 import org.codehaus.jackson.JsonGenerator;
@@ -112,6 +113,8 @@ public class JsonSerializer implements ResultSerializer {
 
     if (resultMetadata.getClass() == DeleteResultMetadata.class) {
       processResultMetadata((DeleteResultMetadata) resultMetadata);
+    } else if (resultMetadata.getClass() == OperationStatusMetaData.class) {
+      processResultMetadata((OperationStatusMetaData) resultMetadata);
     } else {
       throw new IllegalArgumentException("ResultDetails is not of type DeleteResultDetails, cannot parse");
     }
@@ -144,6 +147,30 @@ public class JsonSerializer implements ResultSerializer {
     m_generator.writeEndObject();
   }
 
+  private void processResultMetadata(OperationStatusMetaData metaData) throws IOException {
+    m_generator.writeStartObject();
+    m_generator.writeObjectFieldStart("operationResults");
+
+    for (OperationStatusMetaData.Result result : metaData.getResults()) {
+      m_generator.writeObjectFieldStart(result.getId());
+      m_generator.writeStringField("status", result.isSuccess() ? "success" : "error");
+
+      if (result.getMessage() != null) {
+        m_generator.writeStringField("message", result.getMessage());
+      }
+
+      if (result.getResponse() != null) {
+        m_generator.writeFieldName("response");
+        m_mapper.writeValue(m_generator, result.getResponse());
+      }
+
+      m_generator.writeEndObject();
+    }
+
+    m_generator.writeEndObject();
+    m_generator.writeEndObject();
+  }
+
   private void processNode(TreeNode<Resource> node) throws IOException {
     if (isObject(node)) {
       m_generator.writeStartObject();
@@ -158,10 +185,11 @@ public class JsonSerializer implements ResultSerializer {
     }
 
     if (isArray(node)) {
-      if (node.getName() != null)
+      if (node.getName() != null) {
         m_generator.writeArrayFieldStart(node.getName());
-      else
+      } else {
         m_generator.writeStartArray();
+      }
     }
 
     for (TreeNode<Resource> child : node.getChildren()) {
@@ -186,11 +214,11 @@ public class JsonSerializer implements ResultSerializer {
   // Determines whether or not the given node is an array
   private boolean isArray(TreeNode<Resource> node) {
     return (node.getObject() == null && node.getName() != null) ||
-            (node.getObject() == null && node.getName() == null &&
-             node.getChildren().size() > 1);
+        (node.getObject() == null && node.getName() == null &&
+            node.getChildren().size() > 1);
   }
 
-  private TreeNode<Map<String, Object>> getTreeProperties (Map<String, Map<String, Object>> propertiesMap) {
+  private TreeNode<Map<String, Object>> getTreeProperties(Map<String, Map<String, Object>> propertiesMap) {
     TreeNode<Map<String, Object>> treeProperties = new TreeNodeImpl<>(null, new LinkedHashMap<>(), null);
 
     for (Map.Entry<String, Map<String, Object>> entry : propertiesMap.entrySet()) {

+ 19 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/services/stackadvisor/StackAdvisorRequest.java

@@ -51,6 +51,7 @@ public class StackAdvisorRequest {
   private Set<RecommendationResponse.ConfigGroup> configGroups;
   private Map<String, String> userContext = new HashMap<>();
   private Map<String, Object> ldapConfig = new HashMap<>();
+  private Boolean gplLicenseAccepted;
 
   public String getStackName() {
     return stackName;
@@ -122,6 +123,13 @@ public class StackAdvisorRequest {
     this.configGroups = configGroups;
   }
 
+  /**
+   * @return true if GPL license is accepted, false otherwise
+   */
+  public Boolean getGplLicenseAccepted() {
+    return gplLicenseAccepted;
+  }
+
   private StackAdvisorRequest(String stackName, String stackVersion) {
     this.stackName = stackName;
     this.stackVersion = stackVersion;
@@ -194,6 +202,17 @@ public class StackAdvisorRequest {
       return this;
     }
 
+    /**
+     * Set GPL license acceptance parameter to request.
+     * @param gplLicenseAccepted is GPL license accepted.
+     * @return stack advisor request builder.
+     */
+    public StackAdvisorRequestBuilder withGPLLicenseAccepted(
+        Boolean gplLicenseAccepted) {
+      this.instance.gplLicenseAccepted = gplLicenseAccepted;
+      return this;
+    }
+
     public StackAdvisorRequestBuilder withLdapConfig(Map<String, Object> ldapConfig) {
       Preconditions.checkNotNull(ldapConfig);
       this.instance.ldapConfig = ldapConfig;

+ 9 - 7
ambari-server/src/main/java/org/apache/ambari/server/api/services/stackadvisor/commands/StackAdvisorCommand.java

@@ -47,6 +47,7 @@ import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorResponse;
 import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRunner;
 import org.apache.ambari.server.controller.RootComponent;
 import org.apache.ambari.server.controller.RootService;
+import org.apache.ambari.server.controller.internal.AmbariServerConfigurationCategory;
 import org.apache.ambari.server.controller.internal.RootServiceComponentConfigurationResourceProvider;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.ambari.server.state.ServiceInfo;
@@ -78,7 +79,6 @@ public abstract class StackAdvisorCommand<T extends StackAdvisorResponse> extend
 
   private static final String GET_HOSTS_INFO_URI = "/api/v1/hosts"
       + "?fields=Hosts/*&Hosts/host_name.in(%s)";
-  static final String LDAP_CONFIGURATION_PROPERTY = "ldap-configuration";
 
   private static final String GET_SERVICES_INFO_URI = "/api/v1/stacks/%s/versions/%s/"
       + "?fields=Versions/stack_name,Versions/stack_version,Versions/parent_stack_version"
@@ -91,11 +91,11 @@ public abstract class StackAdvisorCommand<T extends StackAdvisorResponse> extend
       + "&services/StackServices/service_name.in(%s)";
 
   private static final String GET_AMBARI_LDAP_CONFIG_URI = String.format("/api/v1/services/%s/components/%s/configurations?%s=%s&fields=%s",
-    RootService.AMBARI.name(),
-    RootComponent.AMBARI_SERVER.name(),
-    RootServiceComponentConfigurationResourceProvider.CONFIGURATION_CATEGORY_PROPERTY_ID,
-    LDAP_CONFIGURATION_PROPERTY,
-    RootServiceComponentConfigurationResourceProvider.CONFIGURATION_PROPERTIES_PROPERTY_ID);
+      RootService.AMBARI.name(),
+      RootComponent.AMBARI_SERVER.name(),
+      RootServiceComponentConfigurationResourceProvider.CONFIGURATION_CATEGORY_PROPERTY_ID,
+      AmbariServerConfigurationCategory.LDAP_CONFIGURATION.getCategoryName(),
+      RootServiceComponentConfigurationResourceProvider.CONFIGURATION_PROPERTIES_PROPERTY_ID);
 
   private static final String SERVICES_PROPERTY = "services";
   private static final String SERVICES_COMPONENTS_PROPERTY = "components";
@@ -107,6 +107,7 @@ public abstract class StackAdvisorCommand<T extends StackAdvisorResponse> extend
   private static final String CONFIGURATIONS_PROPERTY = "configurations";
   private static final String CHANGED_CONFIGURATIONS_PROPERTY = "changed-configurations";
   private static final String USER_CONTEXT_PROPERTY = "user-context";
+  private static final String GPL_LICENSE_ACCEPTED = "gpl-license-accepted";
   private static final String AMBARI_SERVER_CONFIGURATIONS_PROPERTY = "ambari-server-properties";
 
   private File recommendationsDir;
@@ -229,7 +230,7 @@ public abstract class StackAdvisorCommand<T extends StackAdvisorResponse> extend
         throw new StackAdvisorException("Unexpected JSON document encountered: missing the Configuration/properties object");
       }
 
-      root.put(LDAP_CONFIGURATION_PROPERTY, ldapConfigurationProperties);
+      root.put(AmbariServerConfigurationCategory.LDAP_CONFIGURATION.getCategoryName(), ldapConfigurationProperties);
     } else if (numConfigs > 1) {
       throw new StackAdvisorException(String.format("Multiple (%s) LDAP configs are found in the DB.", numConfigs));
     }
@@ -269,6 +270,7 @@ public abstract class StackAdvisorCommand<T extends StackAdvisorResponse> extend
 
     JsonNode userContext = mapper.valueToTree(request.getUserContext());
     root.put(USER_CONTEXT_PROPERTY, userContext);
+    root.put(GPL_LICENSE_ACCEPTED, request.getGplLicenseAccepted());
   }
 
   private void populateConfigGroups(ObjectNode root,

Some files were not shown because too many files changed in this diff