/* ==================================================================
* Created [2009-4-27 下午11:32:55] by Jon.King
* ==================================================================
* TSS
* ==================================================================
* mailTo:jinpujun@hotmail.com
* Copyright (c) Jon.King, 2009-2012
* ==================================================================
*/
package com.jinhe.tss.portal.engine.releasehtml;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.filters.LinkRegexFilter;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
import com.jinhe.tss.core.common.progress.FeedbackProgressable;
import com.jinhe.tss.core.common.progress.Progress;
import com.jinhe.tss.core.util.FileHelper;
import com.jinhe.tss.portal.EnvionmentVariables;
/**
* <p> MagicRobot.java </p>
* 魔幻发布机器人。
* <br/>分两种情况:
* <br/>1、整站发布:从首页开始发布整个站点。
* <br/>2、单页发布:根据指定的发布页,发布其及其页面上的所有链接,但页面上链接发布出来的页面上的链接不再继续发布。
* <br/>(两者都不处理外挂js、css样式以及图片信息,只发布内容)
*
* <br/>为提高性能,避免对一些不变的页面进行不必要的重新发布(比如文章页),特定以下规则:
* <br/>1、如果已存在的发布页面列表(existsFiles)不包含当前要发布的页面,则发布该页面
* <br/>2、如果包含,且非文章页,则发布该页面
* <br/>3、如果包含,且是文章页,且非第一个发布页面,则不再发布该页面
* <br/>4、如果包含,且是文章页,且是第一个发布页面(单个文章页发布时),则发布该页面
* <br/>
* <br/>另外对是否发布页面上链接地址,也定以下规则:
* <br/>1、如果是整站发布,当前发布页面非文章页,则发布其上所有链接
* <br/>2、如果是整站发布,当前发布页面为文章页,则不发布其上所有链接
* <br/>3、如果是单页发布,且当前发布页是指定页本身,则发布其上所有链接
* <br/>4、如果是单页发布,当前发布页非指定页本身,则不发布其上所有链接
* <br/>
*/
public class MagicRobot extends SimpleRobot implements FeedbackProgressable {
// 整站发布时后缀统一为htm,防止和单个发布出来的页面冲突
protected final static String HTM_FILE_SUFFIX = ".htm";
private final static String PAGE_ISSUING_TAG = "processing";
/**
* Portal页面访问路径 对 发布后的静态路径,发布过程中(发布路径未生成)前,值一律为”processing“
*/
private Map<String, String> urlMapping = new HashMap<String, String>();
/**
* 发布成功生成的html页面名
*/
private List<String> htmlFiles = new ArrayList<String>();
/**
* 发布失败的页面信息
*/
private Map<String, String> issueFailedUrls = new HashMap<String, String>();
/**
* 发布完成后返回信息
*/
private String feedback;
private Long portalId;
/**
* 已经存在的发布页面列表
*/
private List<File> existsFiles;
/**
* 本次发布页面的路径,一般根据当天时间 按 年/月/日 格式来命名
*/
private String pageDir;
/**
* 是否是单页发布 true:单页发布 false:整站发布
*/
private boolean isOnlyPublishPage;
/**
* 发布普通页和普通页的链接,不深入发布
* @param commonPage 页面动态访问地址
*/
public MagicRobot(String commonPage){
super(commonPage);
this.existsFiles = FileHelper.listFilesByTypeDeeply(HTM_FILE_SUFFIX, new File(issuePath));
this.pageDir = createPageDir(super.issuePath);
this.isOnlyPublishPage = true;
this.indexPage = commonPage;
}
/**
* 发布整个门户
* @param portalId 门户ID
* @param indexPage 主页
*/
public MagicRobot(Long portalId){
this("");
this.portalId = portalId;
this.isOnlyPublishPage = false;
this.indexPage = EnvionmentVariables.getContextPath() + "/portal!previewPortal.action?portalId=" + portalId;
}
private Progress progress = new Progress(-1);
public void execute(Map<String, Object> params, Progress progress) {
this.progress = progress;
start();
}
private int count = 0;
/**
* 更新进度信息
*/
private void updateProgressInfo(){
if(++count % 30 == 0){
progress.add(30); //每30个更新一次进度信息
}
}
/**
* 从首页开始静态发布
*/
public void start() {
//执行抓取页面过程
//从首页开始发布
excute(this.indexPage, null);
// 替换页面中的链接
for( String htmlFilePath : htmlFiles ){
File htmlFile = new File(htmlFilePath);
// 如果是上次已经发布的文章页,则无需再替换页面里的地址
String fileName = htmlFile.getName();
if(isArticlePage(fileName) && getExsitingFile(fileName) != null){
continue;
}
replaceUrl(htmlFile); // 替换页面中的链接
}
if(!issueFailedUrls.isEmpty()) {
StringBuffer sb = new StringBuffer("发布结束,但有页面发布出错。具体错误信息如下:\n");
for( Entry<String, String> entry : issueFailedUrls.entrySet() ){
sb.append("地址(").append(entry.getKey()).append(")").append(entry.getValue()).append("\n");
}
feedback = sb.toString();
log.info(feedback);
} else {
feedback = "静态发布成功";
}
// 如果发布结束了进度还没有完成
if(!progress.isCompleted()) {
progress.add(8888888); // 通过设置一个大数(远大于总数)来使进度完成
}
}
/**
* 执行页面的抓取。
* 首先抓取页面内容到本地,然后再将页面中在抓取页面中的地址。
* 递归循环,一直抓取下去。
*
* 以下几种情况的链接地址除外:
* 1."http://"打头的,这个一般为门户以外的链接
* 2、javascript打头的,这种目前难以处理
* 3、"#"打头的或者为空的
*
* @param href
* @param parentHref
* @param issueDeeply 是否发布页面里的链接地址
*/
private void excute(String href, String parentHref) {
boolean isFirstHref = (parentHref == null); //是否是本次发布的第一个页面,即进入页
boolean issueDeeply = isFirstHref || !isOnlyPublishPage; // 单页发布时首次发布 或 整站发布, 则都进行深度发布
if(href == null || href.length() <= 6
|| (href.startsWith("http://") && !isFirstHref) // 非单页发布时链接地址以http://打头的一般为外部网站地址,不做处理
|| href.startsWith("javascript") || href.startsWith("#")
|| href.startsWith("mailto") || href.indexOf(".htm") > 0) {
return;
}
// 如果当前地址已经发布过(不管发布成功或失败),则不再发布
if (urlMapping.containsKey(href) || issueFailedUrls.containsKey(href))
return;
//将要发布的地址设置为正在发布状态,如此其它地方有相同的地址时可以跳过,以免重复发布
urlMapping.put(href, PAGE_ISSUING_TAG);
String url;
if(!href.startsWith(EnvionmentVariables.getContextPath()))
url = contextPath + EnvionmentVariables.getContextPath() + "/" + href;
else
url = contextPath + href;
//生成文件名
String pageName;
if(portalId != null && (isFirstHref || indexPage.equals(href))){ // 判断是否首页
pageName = "index" + this.portalId + HTM_FILE_SUFFIX;
} else {
pageName = genPageName(url, HTM_FILE_SUFFIX);
}
boolean isArticlePage = isArticlePage(pageName);
String htmlFileName = getExsitingFile(pageName);
boolean isExsitingFile = htmlFileName != null;
if(!isExsitingFile) {
//文章页面放到当天发布路径下,以防止单一路径下文件数量过多
htmlFileName = issuePath + (isArticlePage ? pageDir + pageName : pageName);
}
// 判断页面是否已经发布过了 以及 是否是文章页。文章页如果已经发布则无需再发布一边,文章列表页则每次都需要重新发布
// 如果是单页发布,则存在的文章页也重新发布
if(!isExsitingFile || isFirstHref || (isExsitingFile && !isArticlePage) || isOnlyPublishPage){
// 下载页面
boolean success = IssueHelper.saveUrlAsLocalFile(url, htmlFileName);
if(!success) {
issueFailedUrls.put(href, "发布页面时抓取HTML内容出错,位于页面:" + parentHref);
return;
}
// 如果是非文章页,或是单页发布,且是深度发布,则解析里面的地址。非单页发布文章时文章页没必要再解析了
if((!isArticlePage || isOnlyPublishPage )&& issueDeeply){
try {
//处理页面里的动态地址,发布出相应的静态页面。
Parser parser = new Parser(htmlFileName);
parser.setEncoding("GBK");
NodeList list = parser.parse(new LinkRegexFilter(""));
for (NodeIterator it = list.elements(); it.hasMoreNodes();) {
LinkTag linkNode = (LinkTag) it.nextNode();
excute(linkNode.getAttribute("href"), href);
}
} catch (Exception e){
issueFailedUrls.put(href, "发布页面时解析出错,位于页面:" + parentHref);
return;
}
}
}
htmlFiles.add(htmlFileName);
String relativeUrl = htmlFileName.substring(htmlFileName.indexOf("html") - 1); //取相对于html目录的地址 如:/html/index62.htm
urlMapping.put(href, EnvionmentVariables.getContextPath() + relativeUrl); // /pms/html/index62.htm
updateProgressInfo();
}
private String getExsitingFile(String fileName){
for( File htmlFile : existsFiles ){
if(htmlFile.getName().equals(fileName))
return htmlFile.getPath();
}
return null;
}
/**
* 替换各种类型的地址,包括<a href="">
* @param htmlFile
* @throws ParserException
*/
private void replaceUrl(File htmlFile) {
StringBuffer sb = new StringBuffer();
try {
Parser parser = new Parser (htmlFile.getPath());
parser.setEncoding("GBK");
NodeList list = parser.parse(null);
for(NodeIterator outIter = list.elements(); outIter.hasMoreNodes();){
Node bigNode = outIter.nextNode();
replaceDynamicUrl(bigNode);
sb.append(bigNode.toHtml());
}
} catch (ParserException e){
issueFailedUrls.put(htmlFile.getPath(), "处理地址出错:" + e.getCause());
return;
}
//TODO 此处做法比较垃圾,有可能的话再改掉
// 因为用htmlParser解析html时,如果js里有html标签,会自动加上一个</script>,需要将其去掉,
// 但万一js里没有,不能影响到body里的
String htmlContent = sb.toString();
int bodyIndex = htmlContent.indexOf("<body");
int scriptIndex = htmlContent.indexOf("</script></");
if(scriptIndex < bodyIndex){
htmlContent = htmlContent.replaceFirst("</script></", "</");
}
FileHelper.writeFile(htmlFile, htmlContent);
}
/**
* 动态内容发布成静态后,将html中的动态的链接地址换成相应生成html页面地址。
* @param htmlFilePath
*/
private void replaceDynamicUrl(Node bigNode) throws ParserException{
NodeList linkElements = new NodeList();
bigNode.collectInto(linkElements, new LinkRegexFilter(""));
for(NodeIterator inIter = linkElements.elements(); inIter.hasMoreNodes();){
LinkTag linkNode = (LinkTag) inIter.nextNode();
String url = linkNode.getAttribute("href");
String pageUrl = (String) urlMapping.get(url);
if(pageUrl != null){
linkNode.setAttribute("href", pageUrl);
}
}
}
/**
* 根据页面名称判断是否是文章页
* @param pageName
* @return
*/
private boolean isArticlePage(String pageName){
return pageName.indexOf("articleId") > -1 || pageName.indexOf("articlePage.portal") > -1;
}
/**
* <p>
* 生成文章页面的发布路径 (根据创建时间确定发布路径)
* </p>
* @param createTime
* @return 格式: 2008/8/20
*/
protected String createPageDir(String issuePath) {
// 得到当前年月日
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String now = sdf.format(new Date());
int index = now.length();
String year = now.substring(0, index - 4);
String month = now.substring(index - 4, index - 2);
String day = now.substring(index - 2, index);
String pageDirPath = year + "/" + month + "/" + day + "/"; // 发布路径 = 发布目录/年/月/日
FileHelper.createDir(issuePath + pageDirPath);
return pageDirPath;
}
public String getFeedback() {
return feedback;
}
public int getExsitFilesNum() {
return this.existsFiles.size();
}
}