- items;
+}
diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Comment.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Comment.java
new file mode 100644
index 000000000..cf3c33448
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Comment.java
@@ -0,0 +1,56 @@
+package run.halo.app.handler.migrate.support.wordpress;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import run.halo.app.handler.migrate.utils.PropertyMappingTo;
+
+/**
+ *
+ * WordPress导出的xml中对于的comment节点下的子节点值将被映射为该类的属性,
+ * 最终会被转换为{@link run.halo.app.model.entity.PostComment}
+ *
+ *
+ * @author guqing
+ * @author ryanwang
+ * @date 2019-11-17 14:01
+ */
+@Data
+public class Comment {
+
+ @JSONField(name = "wp:comment_id")
+ @PropertyMappingTo("id")
+ private String commentId;
+
+ @JSONField(name = "wp:comment_author")
+ @PropertyMappingTo("author")
+ private String commentAuthor;
+
+ @JSONField(name = "wp:comment_author_email")
+ @PropertyMappingTo("email")
+ private String commentAuthorEmail;
+
+ @JSONField(name = "wp:comment_author_url")
+ @PropertyMappingTo("authorUrl")
+ private String commentAuthorUrl;
+
+ @JSONField(name = "wp:comment_author_IP")
+ @PropertyMappingTo("ipAddress")
+ private String commentAuthorIp;
+
+ @JSONField(name = "wp:comment_date")
+ private String commentDate;
+
+ @JSONField(name = "wp:comment_content")
+ @PropertyMappingTo("content")
+ private String commentContent;
+
+ @JSONField(name = "wp:comment_approved")
+ private String commentApproved;
+
+ @JSONField(name = "wp:comment_parent")
+ @PropertyMappingTo("parentId")
+ private Long commentParent;
+
+ @JSONField(name = "wp:comment_user_id")
+ private String commentUserId;
+}
diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Item.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Item.java
new file mode 100644
index 000000000..e81c4c297
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Item.java
@@ -0,0 +1,60 @@
+package run.halo.app.handler.migrate.support.wordpress;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import run.halo.app.handler.migrate.utils.PropertyMappingTo;
+import run.halo.app.model.entity.BasePost;
+
+import java.util.List;
+
+/**
+ * WordPress导出的xml中对于的item子节点的值将会被映射到该类的属性上,最终被解析为文章属性{@link BasePost}
+ *
+ * @author guqing
+ * @author ryanwang
+ * @date 2019-11-17 13:59
+ */
+@Data
+public class Item {
+
+ private String title;
+
+ private String pubDate;
+
+ private String description;
+
+ @JSONField(name = "content:encoded")
+ @PropertyMappingTo("formatContent")
+ private String content;
+
+ @JSONField(name = "excerpt:encoded")
+ @PropertyMappingTo("summary")
+ private String excerpt;
+
+ @JSONField(name = "wp:post_date")
+ private String postDate;
+
+ @JSONField(name = "wp:comment_status")
+ private String commentStatus;
+
+ @JSONField(name = "wp:post_name")
+ @PropertyMappingTo("url")
+ private String postName;
+
+ @JSONField(name = "wp:status")
+ private String status;
+
+ @JSONField(name = "wp:post_password")
+ @PropertyMappingTo("password")
+ private String postPassword;
+
+ @JSONField(name = "wp:is_sticky")
+ @PropertyMappingTo("topPriority")
+ private Integer isSticky;
+
+ @JSONField(name = "wp:comment")
+ private List comments;
+
+ @JSONField(name = "category")
+ private List categories;
+}
diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Rss.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Rss.java
new file mode 100644
index 000000000..c1f84a5f7
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Rss.java
@@ -0,0 +1,15 @@
+package run.halo.app.handler.migrate.support.wordpress;
+
+import lombok.Data;
+
+/**
+ * WordPress导出的xml数据中对应的rss节点下的子节点channel将会被映射到该类属性
+ * 如果不写这个Rss类包装想要的Channel,会无法解析到想要的结果
+ *
+ * @author guqing
+ * @date 2020-01-17 00:45
+ */
+@Data
+public class Rss {
+ private Channel channel;
+}
diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/WpCategory.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/WpCategory.java
new file mode 100644
index 000000000..e020c318a
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/WpCategory.java
@@ -0,0 +1,23 @@
+package run.halo.app.handler.migrate.support.wordpress;
+
+import lombok.Data;
+import run.halo.app.handler.migrate.utils.PropertyMappingTo;
+import run.halo.app.model.entity.PostCategory;
+
+/**
+ * WordPress导出的xml数据中对应的category的子节点将会被映射到该类属性,
+ * 最终会被转换为文章分类{@link PostCategory}
+ *
+ * @author guqing
+ * @date 2020-01-18 16:09
+ */
+@Data
+public class WpCategory {
+ private String domain;
+
+ @PropertyMappingTo("slugName")
+ private String nicename;
+
+ @PropertyMappingTo("name")
+ private String content;
+}
diff --git a/src/main/java/run/halo/app/handler/migrate/utils/PropertyMappingTo.java b/src/main/java/run/halo/app/handler/migrate/utils/PropertyMappingTo.java
new file mode 100644
index 000000000..4b6cb5e2d
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/migrate/utils/PropertyMappingTo.java
@@ -0,0 +1,21 @@
+package run.halo.app.handler.migrate.utils;
+
+import java.lang.annotation.*;
+
+/**
+ * 该注解用于定义两个对象之间的属性映射关系
+ *
+ * @author guqing
+ * @date 2020-1-19 13:51
+ */
+@Target({ElementType.FIELD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface PropertyMappingTo {
+ /**
+ * value对应的是目标对象的属性名称
+ *
+ * @return 返回源对象属性对应的目标对象的属性名
+ */
+ String value() default "";
+}
diff --git a/src/main/java/run/halo/app/handler/migrate/utils/RelationMapperUtils.java b/src/main/java/run/halo/app/handler/migrate/utils/RelationMapperUtils.java
new file mode 100644
index 000000000..a5c799dd3
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/migrate/utils/RelationMapperUtils.java
@@ -0,0 +1,128 @@
+package run.halo.app.handler.migrate.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 关系映射工具类,用于使用@PropertyMappingTo注解映射源对象与目标对象属性之间的关系
+ *
+ * @author guqing
+ * @date 2020-01-19 01:47
+ */
+public class RelationMapperUtils {
+ private RelationMapperUtils() {
+ }
+
+ public static TARGET convertFrom(SOURCE source, Class targetClazz) {
+ Map methodNameAndTypeMap = new HashMap<>(16);
+ TARGET target;
+ try {
+ // 通过类的详情信息,创建目标对象 这一步等同于Hello target = new Hello();
+ target = targetClazz.getConstructor().newInstance();
+ Field[] targetFields = targetClazz.getDeclaredFields();
+ for (Field field : targetFields) {
+ methodNameAndTypeMap.put(field.getName(), field.getType());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("目标对象创建失败");
+ }
+
+ return propertyMapper(source, target, methodNameAndTypeMap);
+ }
+
+ private static TARGET propertyMapper(SOURCE source, TARGET target, Map methodNameAndTypeMap) {
+ // 判断传入源数据是否为空,如果空,则抛自定义异常
+ if (null == source) {
+ throw new IllegalArgumentException("数据源不能为空");
+ }
+ // 获取源对象的类的详情信息
+ Class> sourceClazz = source.getClass();
+ // 获取源对象的所有属性
+ Field[] sourceFields = sourceClazz.getDeclaredFields();
+
+ // 循环取到源对象的单个属性
+ for (Field sourceField : sourceFields) {
+ boolean sourceFieldHasAnnotation = sourceField.isAnnotationPresent(PropertyMappingTo.class);
+
+ String sourceFieldName = sourceField.getName();
+ // 如果源对象没有添加注解则默认使用源对象属性名去寻找对于的目标对象属性名
+ if (sourceFieldHasAnnotation) {
+ PropertyMappingTo propertyMapping = sourceField.getAnnotation(PropertyMappingTo.class);
+ String targetFieldName = propertyMapping.value();
+ copyProperty(sourceFieldName, targetFieldName, source, target, sourceField.getType(), methodNameAndTypeMap);
+ } else if (methodNameAndTypeMap.containsKey(sourceFieldName)) {
+ // 如果源对象和目标对象的属性名相同则直接设置
+ // 获取目标对象的属性名,将属性名首字母大写,拼接如:setUsername、setId的字符串
+ // 判断源对象的属性名、属性类型是否和目标对象的属性名、属性类型一致
+ copyProperty(sourceFieldName, sourceFieldName, source, target, sourceField.getType(), methodNameAndTypeMap);
+ }
+ }
+ // 返回赋值得到的目标对象
+ return target;
+ }
+
+ private static void copyProperty(String sourceFieldName, String targetFieldName,
+ SOURCE source, TARGET target, Class> sourceType,
+ Map methodNameAndTypeMap) {
+ try {
+ Class> sourceClazz = source.getClass();
+ Class> targetClazz = target.getClass();
+ // 获取源对象的属性名,将属性名首字母大写,拼接如:getUsername、getId的字符串
+ String sourceGetMethodName = getterName(sourceFieldName);
+ // 获得属性的get方法
+ Method sourceMethod = sourceClazz.getMethod(sourceGetMethodName);
+ // 调用get方法
+ Object sourceFieldValue = sourceMethod.invoke(source);
+
+ Class methodType = methodNameAndTypeMap.get(targetFieldName);
+ // 通过字段名称得到set方法名称
+ String targetSetMethodName = setterName(targetFieldName);
+ if (methodType == sourceType) {
+ // 调用方法,并将源对象get方法返回值作为参数传入
+ Method targetSetMethod = targetClazz.getMethod(targetSetMethodName, sourceType);
+ targetSetMethod.invoke(target, sourceFieldValue);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("转换失败,请检查属性类型是否匹配");
+ }
+ }
+
+ /**
+ * 通过属性名称得到对于的get或set方法
+ *
+ * @param fieldName 属性名称
+ * @param methodType 方法类型可选值为:get或set
+ * @return 返回对象的get或set方法的名称
+ */
+ private static String getMethodNameFromField(String fieldName, String methodType) {
+ if (fieldName == null || fieldName.length() == 0) {
+ return null;
+ }
+
+ /*
+ * If the second char is upper, make 'get' + field name as getter name. For example, eBlog -> getBlog
+ */
+ if (fieldName.length() > 2) {
+ String second = fieldName.substring(1, 2);
+ if (second.equals(second.toUpperCase())) {
+ return methodType + fieldName;
+ }
+ }
+
+ // Common situation
+ fieldName = methodType + fieldName.substring(0, 1).toUpperCase() +
+ fieldName.substring(1);
+ return fieldName;
+ }
+
+ private static String getterName(String fieldName) {
+ return getMethodNameFromField(fieldName, "get");
+ }
+
+ private static String setterName(String fieldName) {
+ return getMethodNameFromField(fieldName, "set");
+ }
+}
diff --git a/src/main/java/run/halo/app/handler/staticdeploy/GitStaticDeployHandler.java b/src/main/java/run/halo/app/handler/staticdeploy/GitStaticDeployHandler.java
new file mode 100644
index 000000000..0b94b0059
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/staticdeploy/GitStaticDeployHandler.java
@@ -0,0 +1,33 @@
+package run.halo.app.handler.staticdeploy;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import run.halo.app.model.enums.StaticDeployType;
+import run.halo.app.service.OptionService;
+
+/**
+ * Git deploy handler.
+ *
+ * @author ryanwang
+ * @date 2019-12-26
+ */
+@Slf4j
+@Component
+public class GitStaticDeployHandler implements StaticDeployHandler {
+
+ private final OptionService optionService;
+
+ public GitStaticDeployHandler(OptionService optionService) {
+ this.optionService = optionService;
+ }
+
+ @Override
+ public void deploy() {
+
+ }
+
+ @Override
+ public boolean supportType(StaticDeployType type) {
+ return StaticDeployType.GIT.equals(type);
+ }
+}
diff --git a/src/main/java/run/halo/app/handler/staticdeploy/NetlifyStaticDeployHandler.java b/src/main/java/run/halo/app/handler/staticdeploy/NetlifyStaticDeployHandler.java
new file mode 100644
index 000000000..dddad86e1
--- /dev/null
+++ b/src/main/java/run/halo/app/handler/staticdeploy/NetlifyStaticDeployHandler.java
@@ -0,0 +1,67 @@
+package run.halo.app.handler.staticdeploy;
+
+import cn.hutool.core.io.FileUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import run.halo.app.model.enums.StaticDeployType;
+import run.halo.app.model.properties.NetlifyStaticDeployProperties;
+import run.halo.app.service.OptionService;
+import run.halo.app.service.StaticPageService;
+
+import java.nio.file.Path;
+
+/**
+ * Netlify deploy handler.
+ *
+ * @author ryanwang
+ * @date 2019-12-26
+ */
+@Slf4j
+@Component
+public class NetlifyStaticDeployHandler implements StaticDeployHandler {
+
+ private final static String DEPLOY_API = "https://api.netlify.com/api/v1/sites/%s/deploys";
+
+ private final OptionService optionService;
+
+ private final RestTemplate httpsRestTemplate;
+
+ private final StaticPageService staticPageService;
+
+ public NetlifyStaticDeployHandler(OptionService optionService,
+ RestTemplate httpsRestTemplate,
+ StaticPageService staticPageService) {
+ this.optionService = optionService;
+ this.httpsRestTemplate = httpsRestTemplate;
+ this.staticPageService = staticPageService;
+ }
+
+ @Override
+ public void deploy() {
+ String domain = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_DOMAIN).toString();
+ String siteId = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_SITE_ID).toString();
+ String token = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_TOKEN).toString();
+
+ HttpHeaders headers = new HttpHeaders();
+
+ headers.set("Content-Type", "application/zip");
+ headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
+
+ Path path = staticPageService.zipStaticPagesDirectory();
+
+ byte[] bytes = FileUtil.readBytes(path.toFile());
+
+ HttpEntity httpEntity = new HttpEntity<>(bytes, headers);
+
+ ResponseEntity