Servlet與JSP項目實戰 — 博客系統(中)

接前一篇博客。這篇文章將專一於分析系統的核心功能。javascript

管理員界面

在這個系統中,普通用戶只能發表評論,只有管理員纔可以建立文章。項目的README中介紹了進入管理員界面的方法:訪問地址/adminhtml

首先來看登陸檢查

前文已經介紹到,web.xml指明全部/admin路徑下的請求都要先通過AdminFilter。觀察它的代碼,就是把請求按如下邏輯處理:java

  • 對於"/admin/login.ftl""/admin/login.do",交給下一級處理。這兩個請求將分別被DynamicFilterAdminLogin類接管。web

  • 對於"/admin",重定向到"/admin/admin_index.ftl"ajax

  • 對於其它/admin路徑下的請求,先調用AbstractServlet.isAdminLogin()檢查有沒有登陸。若是有則交給下一級處理;不然重定向到"/admin/login.ftl"要求登陸。數據庫

邏輯很清晰。AdminLogin類的邏輯也很簡單,比對密碼的哈希和配置是否一致。若是一致,就重定向到"/admin/admin_index.ftl",而且在這以前,在Session中設置admin屬性;不然,就重定向到"/admin/login.ftl"要求從新登陸。app

可想而知AbstractServlet.isAdminLogin()的邏輯,就是檢查Session中的admin屬性。webapp

管理後臺

管理員主頁很是簡單,只有以下幾個功能。編輯器

後臺

點擊不一樣的連接會跳轉到不一樣的FTL頁面。例如「最新回覆」,它的地址是admin/new_comment.ftlide

com.zuoxiaolong.dynamic包中找到上面地址對應的動態數據類NewComment,它的代碼很是簡單,就是從數據庫中找到評論,而後交給模板去呈現:

@Namespace("admin")
public class NewComment implements DataMap {
    
    @Override
    public void putCustomData(Map<String, Object> data,HttpServletRequest request, HttpServletResponse response) {
        data.put("newComments", DaoFactory.getDao(CommentDao.class).getComments());
    }
}

編輯文章

在文章管理頁面,經過新建文章能夠進入admin/article_input.ftl頁面。

這個頁面使用了著名的在線文本編輯器TinyMCE。簡單的說,它只是一個JS庫,可以「所見即所得」地把在編輯框中輸入的富文本翻譯成HTML文本。

下面來分析這個FTL文件的代碼。

在正文部分定義了一個textarea,做爲編輯器的佔位元素。

<textarea class="html_editor" style="width:100%"></textarea>

在正文結尾處有以下Javascript代碼:

<script type="text/javascript">
    var settings = {width:900,height:400,content:''};
    <#if article?? && article.escapeHtml??>
    settings.content = '${article.escapeHtml}';
    </#if>
    tinymceInit(settings);
    $(document).ready(function(){
        $("#submitButton").click(function(){
            ...
            $.ajax({
                url:"${contextPath}/admin/updateArticle.do",
                data:{"id":$("input[name=id]").val(),"content":tinymce.activeEditor.getContent()
                    ,"subject":$("input[name=subject]").val(),"status":status,"type":$("select[name=type]").val()
                    ,"tags":$("input[name=tags]").val(),"categories":categories,"updateCreateTime":updateCreateTime},
                type:"POST",
                success:function(data){
                    if(data && data == 'success') {
                        alert("保存成功");
                        window.location.href="${contextPath}/admin/article_manager.ftl"
                    } else {
                        alert("保存失敗");
                    }
                }
            });
        });
    });
</script>

其中tinymceInit(settings);這句話就是初始化TinyMCE的代碼,把編輯器顯示出來。settings.content = '${article.escapeHtml}'; 是把編輯器中的內容設置爲已有的內容——由於更新文章也是用的這個模板頁面。至於文章的HTML內容是從哪裏獲得的,咱們後面再談。

後面的這段AJAX代碼意思是當點擊保存按鈕時,把文章的全部數據提交到"admin/updateArticle.do"這個Url上來,注意代碼使用了tinymce.activeEditor.getContent()來得到編輯器產生的HTML文本。咱們再來看看這個Url對應的處理類作的事情:

@RequestMapping("/admin/updateArticle.do")
public class AdminUpdateArticle extends AbstractServlet {

    @Override
    protected void service() throws ServletException, IOException {
        String id = getRequest().getParameter("id");
        String subject = getRequest().getParameter("subject");
        String html = getRequest().getParameter("content");
        String status = getRequest().getParameter("status");
        String type = getRequest().getParameter("type");
        String icon = getRequest().getParameter("icon");
        String updateCreateTime = getRequest().getParameter("updateCreateTime");
        String[] categories = getRequest().getParameter("categories").split(",");
        String[] tags = getRequest().getParameter("tags").split(",");
        html = handleQuote(html);
        
        StringBuffer stringBuffer = new StringBuffer();
        JsoupUtil.appendText(Jsoup.parse(html), stringBuffer);
        Integer articleId = 
                DaoFactory.getDao(ArticleDao.class).saveOrUpdate(id, 
                        subject, 
                        Status.valueOf(Integer.valueOf(status)), 
                        Type.valueOf(Integer.valueOf(type)), 
                        Integer.valueOf(updateCreateTime), 
                        "左瀟龍", html, stringBuffer.toString(), icon);
...

主要的邏輯就是把數據從請求中讀出來,作一些檢查和處理後存入數據庫。

儘管建立新文章和編輯已有文章共享同一套邏輯,可是在DAO層保存到數據庫時,前者是insert,後者是update。另外建立新文章的時候並不知道文章的Id,因此DAO層插入記錄時將得到數據庫自動生成的ID。

// ArticleDao 的代碼
statement = connection.prepareStatement(insertSql,Statement.RETURN_GENERATED_KEYS);

更新文章時如何得到已有文章的內容

在admin主頁中點擊文章管理能夠進入文章管理頁面(admin/article_manager.ftl)。點擊每篇文章的標題就能夠進入編輯文章頁面,但這個Url還帶了一個參數:文章id

article_input.ftl頁面做爲一個FTL模板文件,天然也會受到DynamicFilter的過濾,與它對應的動態數據類是ArticleInput類。這個類的putCustomData()方法會從請求中拿到id參數,而後根據這個id讀取數據庫,把對應的文章的數據存放到FreeMarker變量"article"中去。模板文件再根據這個參數呈現內容。

上傳圖片

不管是編輯文章仍是提交評論,編輯器都支持上傳圖片的功能。如何實現的呢?

在項目中使用的TinyMCE 4.1.10版本自身還不支持上傳圖片,只能藉助一些插件實現。而這裏的實現另闢蹊徑。咱們來看看位於webapp/resources/js/tinymce路徑下的tinymce.init.js文件。

function tinymceInit(settings) {
    $(document).ready(function() {
        var defaultSettings = {width:600,height:400,content:'',skin:'lightgray'};
        $.extend(defaultSettings,settings);
        tinymce.init({
            selector: "textarea.html_editor",
            language: "zh_CN",
            menubar : false,
            skin: defaultSettings.skin,
            width: defaultSettings.width,
            height: defaultSettings.height,
            toolbar_items_size:'small',
            setup: function(editor) {
                editor.addButton('upload',
                {
                    icon: 'print',
                    title: '上傳本地圖片',
                    onclick: function() {
                        editor.windowManager.open({
                            title: "上傳本地圖片",
                            url: contextPath + "/html/upload_image.html",
                            width: 400,
                            height: 150
                        });
                    }
                });
                editor.addButton('insertcode',
                {
                ...

這個文件是咱們本身創建的,函數tinymceInit()在前面講到的article_input.ftl文件中被調用。它調用了真正的API tinymce.init()來完成初始化。能夠看到函數中調用了editor.addButton()來添加一個上傳圖片的按鈕,被點擊時的行爲則是彈出一個upload_image.html頁面。

upload_image.html頁面是在運行時根據模板文件common/upload_image.ftl生成的。至於如何生成,且聽下回分解。它的主要內容以下:

<body>
    <table class="float_left" style="width: 340px;height: 90px;border: 1px solid #d5d5d5;margin: 5px;">
            <form id="upload_image_form" method="POST" action="http://localhost:8080/uploadImage.do" enctype="multipart/form-data">
                <tr>
                    <td class="form_input">
                        <a class="file_input_a" href="#">
                            選擇圖片
                            <input class="file_input" type="file" name="imageFile" />
                        </a>
                    </td>
                </tr>
                <tr>
                    <td class="form_input">
                        <input type="text" class="text_input" id="file_path" readonly="readonly" style="width:340px;max-width: 340px;"/>
                    </td>
                </tr>
                <tr>
                    <td class="form_input">
                        <input type="submit" class="form_button" value="上傳"/>
                    </td>
                </tr>
            </form>
        </table>
...

<script type="application/javascript">
        $(document).ready(function(){
            $("input[name=imageFile]").change(function(){
                $("#file_path").val($(this).val());
            });
            $("#upload_image_form").ajaxForm({
                beforeSubmit:function(){
                    if(!$("input[name=imageFile]").val()) {
                        window.parent.alert("請選擇圖片");
                        return false;
                    }
                    return true;
                },
                success:function(url){
                    if (url && url == 'format_error') {
                        alert("只能上傳png,jpg,gif格式的文件");
                        return;
                    }
                    if (url) {
                        top.tinymce.activeEditor.insertContent("<img src='" + url + "'/>");
                        top.tinymce.activeEditor.windowManager.close();
                    }
                }
            });
        });
</script>

就是一個上傳文件的表單,目的地址是uploadImage.do。上傳圖片成功後,獲得一個圖片的Url。JS腳本再調用top.tinymce.activeEditor.insertContent()向編輯器中插入圖片元素。

負責處理uploadImage.do的是UploadImage類。它使用commons-fileupload包來獲取文件,保存到image/路徑下,再返回動態生成後的真實Url。

整個上傳過程到此就結束了。再看tinymce.init.js文件能夠發現它還增長了一個插入代碼的按鈕,原理都是相似的。

評論、投票與統計

直接來看blog/article.ftl文件。文章正文和評論部分在article_list.ftl中。

在文章的尾部有一排投票的表情,只能選一個,數據最終提交到數據庫中的article表裏。若是看一下article表的設計,能夠發現它很長。其中不只包含內容、日期這種信息,還包含評論數量等等。

當訪問article頁面時,全部與文章相關的數據都被數據庫中讀取到,顯示到頁面中。點擊頁面中的評論、投票等按鈕時,就會發送請求到相應的xxx.do地址,請求被合適的Servlet處理,最終反映到數據庫中。這些都略去不表。

下面來看下如何跟蹤文章的訪問次數。能夠注意到article表中有一個access_times字段,就是文章的訪問次數。那麼它是如何更新的呢?

article.ftl文件中有這麼一段Javascript腳本:

<script type="text/javascript">
    // 創建評論區的編輯器
    tinymceInit({width:700,height:150,skin:'comment'});
    $(document).ready(function() {
        counter({"articleId":$("#articleId").val(),"type":1,"column":"access_times"});
        ...

這裏的counter()函數像是跟訪問計數有關。它在common/common.js中定義:

function counter(data) {
    $.ajax({
        url:contextPath + "/counter.do",
        type:"POST",
        data:data
    });
}

就是把數據發到"/counter.do"地址。來看它的處理類Counter:

public class Counter extends AbstractServlet {

    @Override
    protected void service() throws IOException {
        HttpServletRequest request = getRequest();
        Integer type = Integer.valueOf(request.getParameter("type"));
        if (type == 1) {
            updateArticle(request);
        } else if (type == 2) {
            updateQuestion(request);
        }  else if (type == 3) {
            updateRecord(request);
        } else {
            throw new RuntimeException("unknown type.");
        }
    }
    
...

    private void updateArticle(HttpServletRequest request) {
        Integer articleId = Integer.valueOf(request.getParameter("articleId"));
        String column = request.getParameter("column");
        if (logger.isInfoEnabled()) {
            logger.info("counter param : articleId = " + articleId + "   , column = " + column);
        }
        if (!column.equals("access_times")) {
            if (logger.isInfoEnabled()) {
                logger.info("there is someone remarking...");
            }
            String username = getUsername();
            String ip = HttpUtil.getVisitorIp(request);
            if (DaoFactory.getDao(ArticleIdVisitorIpDao.class).exists(articleId, ip, username)) {
                writeText("exists");
                if (logger.isInfoEnabled()) {
                    logger.info(ip + " has remarked...");
                }
                return ;
            } else {
                DaoFactory.getDao(ArticleIdVisitorIpDao.class).save(articleId, ip, username);
            }
        }
        boolean result = DaoFactory.getDao(ArticleDao.class).updateCount(articleId, column);
...

這裏的邏輯是,判斷一下同一個IP和User是否是已經訪問過這篇文章了。若是沒有,就把訪問歷史先保存起來,再調用Dao層把access_times加1。

相關文章
相關標籤/搜索