This is an automated email from the ASF dual-hosted git repository.

nju_yaho pushed a commit to tag ebay-3.1.0-release-20200701
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit db2a22c56705ade98f6b05d3851ca2b9db61ed72
Author: Wang gang <gwa...@ebay.com>
AuthorDate: Mon Jun 22 12:20:26 2020 +0800

    KYLIN-4594 Send out notification email when triggering cube building job 
and finding out table schema changes
---
 .../org/apache/kylin/rest/service/JobService.java  |  49 +++-
 .../rest/service/TableSchemaUpdateChecker.java     | 139 +++++++++--
 .../mail_templates/HIVE_SCHEMA_CHANGED.ftl         | 264 +++++++++++++++++++++
 3 files changed, 417 insertions(+), 35 deletions(-)

diff --git 
a/server-base/src/main/java/org/apache/kylin/rest/service/JobService.java 
b/server-base/src/main/java/org/apache/kylin/rest/service/JobService.java
index a214c12..f9880bd 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/JobService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/JobService.java
@@ -33,7 +33,6 @@ import java.util.TimeZone;
 
 import javax.annotation.Nullable;
 
-import com.google.common.collect.Maps;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.lock.DistributedLock;
@@ -51,8 +50,8 @@ import 
org.apache.kylin.engine.mr.BatchOptimizeJobCheckpointBuilder;
 import org.apache.kylin.engine.mr.CubingJob;
 import org.apache.kylin.engine.mr.LookupSnapshotBuildJob;
 import org.apache.kylin.engine.mr.LookupSnapshotJobBuilder;
-import org.apache.kylin.engine.mr.common.CubeJobLockUtil;
 import org.apache.kylin.engine.mr.StreamingCubingEngine;
+import org.apache.kylin.engine.mr.common.CubeJobLockUtil;
 import org.apache.kylin.engine.mr.common.JobInfoConverter;
 import org.apache.kylin.engine.mr.steps.CubingExecutableUtil;
 import org.apache.kylin.job.JobInstance;
@@ -70,21 +69,31 @@ import org.apache.kylin.job.execution.CheckpointExecutable;
 import org.apache.kylin.job.execution.DefaultChainedExecutable;
 import org.apache.kylin.job.execution.ExecutableState;
 import org.apache.kylin.job.execution.Output;
+import org.apache.kylin.job.lock.zookeeper.ZookeeperJobLock;
 import org.apache.kylin.job.util.MailNotificationUtil;
 import org.apache.kylin.metadata.model.SegmentRange;
 import org.apache.kylin.metadata.model.SegmentRange.TSRange;
 import org.apache.kylin.metadata.model.SegmentStatusEnum;
 import org.apache.kylin.metadata.model.Segments;
 import org.apache.kylin.metadata.model.TableDesc;
+import org.apache.kylin.metadata.model.TableExtDesc;
+import org.apache.kylin.metadata.model.TableRef;
 import org.apache.kylin.metadata.realization.RealizationStatusEnum;
 import org.apache.kylin.rest.exception.BadRequestException;
 import org.apache.kylin.rest.msg.Message;
 import org.apache.kylin.rest.msg.MsgPicker;
+import org.apache.kylin.rest.response.ResponseCode;
 import org.apache.kylin.rest.util.AclEvaluate;
+import org.apache.kylin.shaded.com.google.common.base.Function;
+import org.apache.kylin.shaded.com.google.common.base.Predicate;
+import org.apache.kylin.shaded.com.google.common.base.Predicates;
+import org.apache.kylin.shaded.com.google.common.collect.FluentIterable;
+import org.apache.kylin.shaded.com.google.common.collect.Lists;
+import org.apache.kylin.shaded.com.google.common.collect.Maps;
+import org.apache.kylin.shaded.com.google.common.collect.Sets;
 import org.apache.kylin.source.ISource;
 import org.apache.kylin.source.SourceManager;
 import org.apache.kylin.source.SourcePartition;
-import org.apache.kylin.job.lock.zookeeper.ZookeeperJobLock;
 import org.apache.kylin.source.hive.MRHiveDictUtil;
 import org.apache.kylin.stream.coordinator.Coordinator;
 import org.apache.kylin.stream.core.model.SegmentBuildState;
@@ -92,17 +101,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
 
-import org.apache.kylin.shaded.com.google.common.base.Function;
-import org.apache.kylin.shaded.com.google.common.base.Predicate;
-import org.apache.kylin.shaded.com.google.common.base.Predicates;
-import org.apache.kylin.shaded.com.google.common.collect.FluentIterable;
-import org.apache.kylin.shaded.com.google.common.collect.Lists;
-import org.apache.kylin.shaded.com.google.common.collect.Sets;
-
 /**
  * @author ysong1
  */
@@ -116,6 +119,10 @@ public class JobService extends BasicService implements 
InitializingBean {
     @Autowired
     private AclEvaluate aclEvaluate;
 
+    @Autowired
+    @Qualifier("tableService")
+    private TableService tableService;
+
     /*
     * (non-Javadoc)
     *
@@ -248,6 +255,7 @@ public class JobService extends BasicService implements 
InitializingBean {
 
         if (buildType == CubeBuildTypeEnum.BUILD || buildType == 
CubeBuildTypeEnum.REFRESH) {
             checkAllowParallelBuilding(cube);
+            checkSourceSchemaUpdate(cube);
         }
 
         DefaultChainedExecutable job;
@@ -293,6 +301,27 @@ public class JobService extends BasicService implements 
InitializingBean {
         return jobInstance;
     }
 
+    private void checkSourceSchemaUpdate(CubeInstance cube) {
+        String[] tables = 
cube.getModel().getAllTables().stream().map(TableRef::getTableIdentity)
+                .toArray(String[]::new);
+        String project = 
getProjectManager().getProjectOfModel(cube.getModel().getName()).getName();
+        try {
+            List<Pair<TableDesc, TableExtDesc>> allMeta = 
tableService.extractHiveTableMeta(tables, project);
+            // do schema check
+            TableSchemaUpdateChecker checker = new 
TableSchemaUpdateChecker(getTableManager(), getCubeManager(),
+                    getDataModelManager());
+            for (Pair<TableDesc, TableExtDesc> pair : allMeta) {
+                TableDesc tableDesc = pair.getFirst();
+                TableSchemaUpdateChecker.CheckResult result = 
checker.allowReload(tableDesc, project);
+                result.raiseExceptionAndNotifyWhenInvalid();
+            }
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException(e.getMessage(), 
ResponseCode.CODE_UNDEFINED, e);
+        } catch (Exception e) {
+            logger.warn("Can not connect to hive, table schema check 
skipped!");
+        }
+    }
+
     public Pair<JobInstance, List<JobInstance>> submitOptimizeJob(CubeInstance 
cube, Set<Long> cuboidsRecommend,
             String submitter) throws IOException, JobException {
 
diff --git 
a/server-base/src/main/java/org/apache/kylin/rest/service/TableSchemaUpdateChecker.java
 
b/server-base/src/main/java/org/apache/kylin/rest/service/TableSchemaUpdateChecker.java
index 3a45f49..c2307ff 100644
--- 
a/server-base/src/main/java/org/apache/kylin/rest/service/TableSchemaUpdateChecker.java
+++ 
b/server-base/src/main/java/org/apache/kylin/rest/service/TableSchemaUpdateChecker.java
@@ -18,19 +18,26 @@
 
 package org.apache.kylin.rest.service;
 
-import static 
org.apache.kylin.shaded.com.google.common.base.Preconditions.checkNotNull;
 import static java.lang.String.format;
+import static 
org.apache.kylin.shaded.com.google.common.base.Preconditions.checkNotNull;
 
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
 import java.util.stream.Collectors;
 
 import javax.annotation.Nullable;
 
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.util.MailService;
+import org.apache.kylin.common.util.MailTemplateProvider;
+import org.apache.kylin.common.util.Pair;
+import org.apache.kylin.common.util.StringUtil;
 import org.apache.kylin.cube.CubeInstance;
 import org.apache.kylin.cube.CubeManager;
+import org.apache.kylin.job.util.MailNotificationUtil;
 import org.apache.kylin.metadata.TableMetadataManager;
 import org.apache.kylin.metadata.model.ColumnDesc;
 import org.apache.kylin.metadata.model.DataModelDesc;
@@ -38,62 +45,90 @@ import org.apache.kylin.metadata.model.DataModelManager;
 import org.apache.kylin.metadata.model.ModelDimensionDesc;
 import org.apache.kylin.metadata.model.TableDesc;
 import org.apache.kylin.metadata.model.TblColRef;
-
+import org.apache.kylin.metadata.project.ProjectManager;
 import org.apache.kylin.shaded.com.google.common.base.Preconditions;
 import org.apache.kylin.shaded.com.google.common.base.Predicate;
 import org.apache.kylin.shaded.com.google.common.collect.ImmutableList;
 import org.apache.kylin.shaded.com.google.common.collect.Iterables;
 import org.apache.kylin.shaded.com.google.common.collect.Lists;
+import org.apache.kylin.shaded.com.google.common.collect.Maps;
 import org.apache.kylin.shaded.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class TableSchemaUpdateChecker {
+    private static final Logger logger = 
LoggerFactory.getLogger(TableSchemaUpdateChecker.class);
+
     private final TableMetadataManager metadataManager;
     private final CubeManager cubeManager;
     private final DataModelManager dataModelManager;
 
     public static class CheckResult {
+        private final String tableName;
         private final boolean valid;
         private final String reason;
+        private final Set<CubeInstance> impactedCubes;
 
-        private CheckResult(boolean valid, String reason) {
+        private CheckResult(String tableName, boolean valid, String reason) {
+            this(tableName, valid, reason, Sets.<CubeInstance> newHashSet());
+        }
+
+        private CheckResult(String tableName, boolean valid, String reason, 
Set<CubeInstance> impactedCubes) {
+            this.tableName = tableName;
             this.valid = valid;
             this.reason = reason;
+            this.impactedCubes = impactedCubes;
         }
 
         public void raiseExceptionWhenInvalid() {
+            raiseExceptionAndNotifyWhenInvalid(false);
+        }
+
+        public void raiseExceptionAndNotifyWhenInvalid() {
+            raiseExceptionAndNotifyWhenInvalid(true);
+        }
+
+        private void raiseExceptionAndNotifyWhenInvalid(boolean ifNotify) {
             if (!valid) {
-                throw new RuntimeException(reason);
+                if (ifNotify) {
+                    notifyInvalid(tableName, reason, impactedCubes);
+                }
+                throw new IllegalArgumentException(reason);
             }
         }
 
         static CheckResult validOnFirstLoad(String tableName) {
-            return new CheckResult(true, format(Locale.ROOT, "Table '%s' 
hasn't been loaded before", tableName));
+            return new CheckResult(tableName, true,
+                    format(Locale.ROOT, "Table '%s' hasn't been loaded 
before", tableName));
         }
 
         static CheckResult validOnCompatibleSchema(String tableName) {
-            return new CheckResult(true,
+            return new CheckResult(tableName, true,
                     format(Locale.ROOT, "Table '%s' is compatible with all 
existing cubes", tableName));
         }
 
         static CheckResult invalidOnFetchSchema(String tableName, Exception e) 
{
-            return new CheckResult(false,
+            return new CheckResult(tableName, false,
                     format(Locale.ROOT, "Failed to fetch metadata of '%s': 
%s", tableName, e.getMessage()));
         }
 
-        static CheckResult invalidOnIncompatibleSchema(String tableName, 
List<String> reasons) {
+        static CheckResult invalidOnIncompatibleSchema(String tableName, 
List<String> reasons,
+                Set<CubeInstance> impactedCubes) {
             StringBuilder buf = new StringBuilder();
             for (String reason : reasons) {
                 buf.append("- ").append(reason).append("\n");
             }
 
-            return new CheckResult(false,
+            return new CheckResult(tableName, false,
                     format(Locale.ROOT,
                             "Found %d issue(s) with '%s':%n%s Please disable 
and " + "purge related " + "cube(s) first",
-                            reasons.size(), tableName, buf.toString()));
+                            reasons.size(), tableName, buf.toString()),
+                    impactedCubes);
         }
     }
 
-    TableSchemaUpdateChecker(TableMetadataManager metadataManager, CubeManager 
cubeManager, DataModelManager dataModelManager) {
+    TableSchemaUpdateChecker(TableMetadataManager metadataManager, CubeManager 
cubeManager,
+            DataModelManager dataModelManager) {
         this.metadataManager = checkNotNull(metadataManager, "metadataManager 
is null");
         this.cubeManager = checkNotNull(cubeManager, "cubeManager is null");
         this.dataModelManager = checkNotNull(dataModelManager, 
"dataModelManager is null");
@@ -195,32 +230,36 @@ public class TableSchemaUpdateChecker {
         }
         List<String> issues = Lists.newArrayList();
 
-        for (DataModelDesc usedModel : findModelByTable(newTableDesc, prj)){
+        for (DataModelDesc usedModel : findModelByTable(newTableDesc, prj)) {
             checkValidationInModel(newTableDesc, issues, usedModel);
         }
 
+        Set<CubeInstance> impactedCubes = Sets.newHashSet();
         for (CubeInstance cube : findCubeByTable(newTableDesc)) {
-            checkValidationInCube(newTableDesc, issues, cube);
+            boolean ifImpacted = checkValidationInCube(newTableDesc, issues, 
cube);
+            if (ifImpacted) {
+                impactedCubes.add(cube);
+            }
         }
 
         if (issues.isEmpty()) {
             return CheckResult.validOnCompatibleSchema(fullTableName);
         }
-        return CheckResult.invalidOnIncompatibleSchema(fullTableName, issues);
+        return CheckResult.invalidOnIncompatibleSchema(fullTableName, issues, 
impactedCubes);
     }
 
     private Iterable<? extends DataModelDesc> findModelByTable(TableDesc 
newTableDesc, String prj) {
         List<DataModelDesc> usedModels = Lists.newArrayList();
         List<String> modelNames = 
dataModelManager.getModelsUsingTable(newTableDesc, prj);
-        modelNames.stream()
-                .map(mn -> dataModelManager.getDataModelDesc(mn))
-                .filter(m -> null != m)
+        modelNames.stream().map(mn -> 
dataModelManager.getDataModelDesc(mn)).filter(m -> null != m)
                 .forEach(m -> usedModels.add(m));
 
         return usedModels;
     }
 
-    private void checkValidationInCube(TableDesc newTableDesc, List<String> 
issues, CubeInstance cube) {
+    private boolean checkValidationInCube(TableDesc newTableDesc, List<String> 
issues, CubeInstance cube) {
+        boolean ifImpacted = false;
+
         final String fullTableName = newTableDesc.getIdentity();
         String modelName = cube.getModel().getName();
         // if user reloads a fact table used by cube, then all used columns 
must match current schema
@@ -230,6 +269,7 @@ public class TableSchemaUpdateChecker {
             if (!violateColumns.isEmpty()) {
                 issues.add(format(Locale.ROOT, "Column %s used in cube[%s] and 
model[%s], but changed " + "in hive",
                         violateColumns, cube.getName(), modelName));
+                ifImpacted = true;
             }
         }
 
@@ -238,11 +278,15 @@ public class TableSchemaUpdateChecker {
         if (cube.getModel().isLookupTable(fullTableName)) {
             TableDesc lookupTable = 
cube.getModel().findFirstTable(fullTableName).getTableDesc();
             if (!checkAllColumnsInTableDesc(lookupTable, newTableDesc)) {
-                issues.add(format(Locale.ROOT, "Table '%s' is used as Lookup 
Table in cube[%s] and model[%s], but "
-                                + "changed in " + "hive, only append operation 
are supported on hive table as lookup table",
+                issues.add(format(Locale.ROOT,
+                        "Table '%s' is used as Lookup Table in cube[%s] and 
model[%s], but " + "changed in "
+                                + "hive, only append operation are supported 
on hive table as lookup table",
                         lookupTable.getIdentity(), cube.getName(), modelName));
+                ifImpacted = true;
             }
         }
+
+        return ifImpacted;
     }
 
     private void checkValidationInModel(TableDesc newTableDesc, List<String> 
issues, DataModelDesc usedModel) {
@@ -281,7 +325,7 @@ public class TableSchemaUpdateChecker {
 
         return violateColumns;
     }
-    
+
     private List<String> checkAllColumnsInFactTable(DataModelDesc usedModel, 
TableDesc factTable,
             TableDesc newTableDesc) {
         List<String> violateColumns = Lists.newArrayList();
@@ -316,8 +360,8 @@ public class TableSchemaUpdateChecker {
 
     private ColumnDesc mustGetColumnDesc(TableDesc factTable, String 
columnName) {
         ColumnDesc columnDesc = factTable.findColumnByName(columnName);
-        Preconditions.checkNotNull(columnDesc,
-                format(Locale.ROOT, "Can't find column %s in current fact 
table %s.", columnName, factTable.getIdentity()));
+        Preconditions.checkNotNull(columnDesc, format(Locale.ROOT, "Can't find 
column %s in current fact table %s.",
+                columnName, factTable.getIdentity()));
         return columnDesc;
     }
 
@@ -348,10 +392,10 @@ public class TableSchemaUpdateChecker {
         List<String> issues = Lists.newArrayList();
         checkAllColumnsInHiveTableDesc(hiveTableDesc, newTableDesc, issues);
         if (issues.isEmpty()) {
-            return new CheckResult(true,
+            return new CheckResult(fullTableName, true,
                     format(Locale.ROOT, "Table '%s' is compatible with 
existing hive table", fullTableName));
         } else {
-            return new CheckResult(false, format(Locale.ROOT,
+            return new CheckResult(fullTableName, false, format(Locale.ROOT,
                     "Table '%s' is incompatible with existing hive table due 
to '%s'", fullTableName, issues));
         }
     }
@@ -390,4 +434,49 @@ public class TableSchemaUpdateChecker {
             issues.add(format(Locale.ROOT, "Columns %s are incompatible " + 
"in hive", violateColumns));
         }
     }
+
+    private static void notifyInvalid(String tableName, String reason, 
Set<CubeInstance> impactedCubes) {
+        KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
+
+        Set<String> users = Sets.newHashSet();
+        for (CubeInstance cube : impactedCubes) {
+            users.addAll(cube.getDescriptor().getNotifyList());
+        }
+        final String[] adminDls = kylinConfig.getAdminDls();
+        if (adminDls != null) {
+            users.addAll(Lists.newArrayList(adminDls));
+        }
+
+        logger.debug("send notification to owner of {} impacted cubes for 
table {}", impactedCubes.size(), tableName);
+        Pair<String, String> mail = formatNotifications(tableName, 
impactedCubes, reason);
+        new MailService(kylinConfig).sendMail(Lists.newArrayList(users), 
mail.getFirst(), mail.getSecond());
+    }
+
+    private static Pair<String, String> formatNotifications(String tableName, 
Set<CubeInstance> cubes,
+            String issueDetails) {
+        KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
+        ProjectManager projectManager = 
ProjectManager.getInstance(kylinConfig);
+
+        Map<String, Object> root = Maps.newHashMap();
+        root.put("job_name", "Source Data Schema Change");
+        root.put("env_name", kylinConfig.getDeployEnv());
+        root.put("change_details", 
Matcher.quoteReplacement(StringUtil.noBlank(issueDetails, "no change info")));
+        root.put("source_type", "HIVE");
+        root.put("source_data", tableName);
+
+        List<Map> impactInfoList = Lists.newArrayList();
+        for (CubeInstance cube : cubes) {
+            Map<String, Object> impactInfoMap = Maps.newHashMap();
+            impactInfoMap.put("project_name", 
projectManager.getProjectOfModel(cube.getModel().getName()));
+            impactInfoMap.put("cube_name", cube.getName());
+            impactInfoMap.put("cube_owner", cube.getOwner());
+            impactInfoList.add(impactInfoMap);
+        }
+
+        root.put("impacted_info_List", impactInfoList);
+
+        String content = 
MailTemplateProvider.getInstance().buildMailContent("HIVE_SCHEMA_CHANGED", 
root);
+        String title = MailNotificationUtil.getMailTitle("TABLE SCHEMA", 
"CHANGED", kylinConfig.getDeployEnv());
+        return Pair.newPair(title, content);
+    }
 }
diff --git a/tool/src/main/resources/mail_templates/HIVE_SCHEMA_CHANGED.ftl 
b/tool/src/main/resources/mail_templates/HIVE_SCHEMA_CHANGED.ftl
new file mode 100644
index 0000000..6e0929a
--- /dev/null
+++ b/tool/src/main/resources/mail_templates/HIVE_SCHEMA_CHANGED.ftl
@@ -0,0 +1,264 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<html xmlns="http://www.w3.org/1999/xhtml";>
+
+<head>
+    <meta http-equiv="Content-Type" content="Multipart/Alternative; 
charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+</head>
+
+<style>
+    html {
+        font-size: 10px;
+    }
+
+    * {
+        box-sizing: border-box;
+    }
+
+    a:hover,
+    a:focus {
+        color: #23527c;
+        text-decoration: underline;
+    }
+
+    a:focus {
+        outline: 5px auto -webkit-focus-ring-color;
+        outline-offset: -2px;
+    }
+</style>
+
+<body>
+<div style="font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+<span style="line-height: 1;font-size: 16px;">
+<p style="text-align:left;">Dear Kylin user,</p>
+<p>We have detected that the schema of source data has the following 
incompatible changes, which affects the usability
+of the cube. Please upgrade your cube ASAP. Thank you! </p>
+</span>
+    <hr style="margin-top: 10px;
+margin-bottom: 10px;
+height:0px;
+border-top: 1px solid #eee;
+border-right:0px;
+border-bottom:0px;
+border-left:0px;">
+
+    <span style="display: inline;
+background-color: #f0ad4e;
+color: #fff;
+line-height: 1;
+font-weight: 700;
+font-size:36px;
+text-align: center;">&nbsp;Warn&nbsp;</span>
+
+    <hr style="margin-top: 10px;
+margin-bottom: 10px;
+height:0px;
+border-top: 1px solid #eee;
+border-right:0px;
+border-bottom:0px;
+border-left:0px;">
+    <table cellpadding="0" cellspacing="0" width="100%" 
style="border-collapse: collapse;border:1px solid #faebcc;">
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+background-color: #fcf8e3;
+border:1px solid #faebcc;">
+                <h4 style="margin-top: 0;
+margin-bottom: 0;
+font-size: 14px;
+color: #8a6d3b;
+font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${job_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+background-color: #fcf8e3;
+border:1px solid #faebcc;">
+                <h4 style="margin-top: 0;
+margin-bottom: 0;
+font-size: 14px;
+color: #8a6d3b;
+font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                ${env_name}
+                </h4>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid 
#ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, 
sans-serif;">
+                    <tr>
+                        <th width="30%" style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="
+margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;">Source Type</h4>
+                        </th>
+                        <td style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;
+font-weight: 300;">
+                            ${source_type}</h4>
+                        </td>
+                    </tr>
+                    <tr>
+                        <th width="30%" style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="
+margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;">Source Data</h4>
+                        </th>
+                        <td style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;
+font-weight: 300;">
+                            ${source_data}</h4>
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+background-color: #fcf8e3;
+border:1px solid #faebcc;">
+                <h4 style="margin-top: 0;
+margin-bottom: 0;
+font-size: 14px;
+color: #8a6d3b;
+font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    Change Details
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid 
#ddd;border-collapse: collapse;table-layout: fixed;font-family: 'Trebuchet MS 
', Arial, Helvetica, sans-serif;">
+                    <tr>
+                        <td style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;
+font-weight: 300;">
+                                <pre style="white-space: 
pre-wrap;">${change_details}</pre>
+                            </h4>
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+
+        <tr>
+
+            <td style="padding: 10px 15px;
+background-color: #fcf8e3;
+border:1px solid #faebcc;">
+                <h4 style="margin-top: 0;
+margin-bottom: 0;
+font-size: 14px;
+color: #8a6d3b;
+font-family: 'Trebuchet MS ', Arial, Helvetica, sans-serif;">
+                    Impacts
+                </h4>
+            </td>
+        </tr>
+        <tr>
+
+            <td style="padding: 15px;">
+                <table cellpadding="0" cellspacing="0" width="100%"
+                       style="margin-bottom: 20px;border:1 solid 
#ddd;border-collapse: collapse;font-family: 'Trebuchet MS ', Arial, Helvetica, 
sans-serif;">
+                <#list impacted_info_List as impacted_info>
+                    <tr>
+                        <td style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;
+font-weight: 300;">
+                            ${impacted_info.project_name}</h4>
+                        </td>
+                        <td style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;
+font-weight: 300;">
+                            ${impacted_info.cube_name}</h4>
+                        </td>
+                        <td style="border: 1px solid #ddd;
+padding: 8px;">
+                            <h4 style="margin-top: 0;
+margin-bottom: 0;
+line-height: 1.5;
+text-align: left;
+font-size: 14px;
+font-style: normal;
+font-weight: 300;">
+                            ${impacted_info.cube_owner}</h4>
+                        </td>
+                    </tr>
+                </#list>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <hr style="margin-top: 20px;
+margin-bottom: 20px;
+height:0px;
+border-top: 1px solid #eee;
+border-right:0px;
+border-bottom:0px;
+border-left:0px;">
+    <h4 style="font-weight: 500;
+line-height: 1;font-size:16px;">
+        <p>Best Wishes!</p>
+        <p style="margin: 0 0 10px;">
+            <a href="mailto:dl-ebay-kylin-c...@ebay.com " style="color: 
#337ab7;text-decoration: none;">
+                eBay ADI Kylin Team
+            </a>
+        </p>
+    </h4>
+</div>
+</body>
+
+</html>
\ No newline at end of file

Reply via email to