接前一篇博客。這篇文章將專一於分析系統的核心功能。javascript
在這個系統中,普通用戶只能發表評論,只有管理員纔可以建立文章。項目的README中介紹了進入管理員界面的方法:訪問地址/admin。html
前文已經介紹到,web.xml指明全部/admin路徑下的請求都要先通過AdminFilter
。觀察它的代碼,就是把請求按如下邏輯處理:java
對於"/admin/login.ftl"和"/admin/login.do",交給下一級處理。這兩個請求將分別被DynamicFilter
和AdminLogin
類接管。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.ftl。ide
在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。