读书人

OMTookit引见(1) 简单示例:OMSimple

发布时间: 2012-12-27 10:17:10 作者: rapoo

OMTookit介绍(1) 简单示例:OMSimpleBlog

OMTookit介绍(1) ?简单示例:OMSimpleBlog

?

  还是之前介绍过的开源项目OMToolkit(http://code.google.com/p/oh-my-toolkit/),在正式介绍OMToolkit的实现方式之前,先以一个简单的例子(OMSimpleBlog)说明OMToolkit的功能。我们以一个空项目开始(包含了必要的目录结构和文件,和空的Database类),请先下载附件中的OMSimpleBlog_Empty.rar,解压后导入到eclipse中。

?

1.Entity建模

?

  首先是Entity的建模。OMSimpleBlog的数据存储结构比较简单,只有Database、User、Article三个用于持久化的Entity。建模方式如下:

?

  Database建模

?

package com.omc.entity;import com.omc.core.*;import com.omc.core.field.*;@SuppressWarnings("unused")public class Database extends Entity {private Children<User> userList;private References<Article> articleList;}

  Database类包含了一个用户列表和一个文章列表。

  Children与References的区别在于,Children表示严格的父子关系:

  1. 当一个Entity被删除时,其所有的Children也将被删除;

  2. 当一个新建的Entity对象被添加到parent的Children中时,该对象在事务提交时将被持久化。

  3. 当一个Entity对象从parent的Children中移除时,该对象在事务提交时将被删除。

  而References仅表示普通的引用关系,与对象的持久化无关。

  实际上,所有被持久化的Entity对象都应该是某个其他Entity对象的Children(或Child)之一—atabase对象除外)。

?

  这里之所以没有把Article作为Children,是因为Article应该是从属于User的,即删除一个用户时,其文章也应该被删除。  另外,*List这种命名方式被用于Entity内置的doAdd()、doDelete()、doList()等方法,如删除一个Entity时,会先获取它的parent,再将自己从parent的*List列表中remove()掉。如果不采用这种方式,就需要自己实现这些方法了。

?

  User建模

?

package com.omc.entity;import com.omc.core.*;import com.omc.core.field.*;@SuppressWarnings("unused")public class User extends Entity {private StringField name;private StringField password;private StringField blogName;private Children<Article> articleList;}

  User类包含了名称、密码、博客名称和文章列表。

  因为这里的博客属性比较简单,只有blogName,所以合并到了User中(否则可以将Blog作为Child<Blog>属性);同时,没有对文章进行分类,所以直接包含了一个文章列表(否则需要包含的是Children<Category>)。

?

  Article建模

?

package com.omc.entity;import com.omc.core.*;import com.omc.core.field.*;@SuppressWarnings("unused")public class Article extends Entity {private DateField published;private StringField title;private TextField summary;private TextField content;private Reference<User> user;}

?

  Article类包含了发布时间、标题、摘要、全文内容和一个对User的引用。

?

  这里需要特别说明的,是TextField这个类型。实际上是继承自Child<Text>,即内部封装了实体Text,这么做的目的是为了利用Child的延迟加载机制,因为我们并非总是需要显示文章的摘要和全文内容,而这两个字段通常又比较长,读取比较耗时。

  同时,OMToolkit针对TextField类,在通过URL赋值、视图渲染时进行了特殊处理,使得它的使用与一般的StringField的使用没有很大区别,甚至还增加了getHtml()方法,使得我们在视图可以使用类似${content.html}的写法显示转为html的全文(不过目前这个方法还不完善,只能进行一些简单的转换)。

?

  这些用于建模的字段(StringField、DateField、Textfield、Children、References等),都继承自OMField,它们都会被持久化存储。而基本类型(String、int、long等)的属性不会被持久化,通常被用作显示相关的属性,这点后面将会提到。

?

  好了,到此为止,就完成了所有需要持久化的Entity的建模,目前的代码应该与附件中的OMSimpleBlog_Step1.rar类似。接下来,我们将实现用户的注册与登录等操作。

?

?

2.用户注册与登录

?

  母版页面

?

  在views/Master文件夹中增加三个母版页面:

?  views/Home/master.html

<html><head><meta http-equiv = "content-type" content = "text/html; charset=GBK" /><title>Blog :#abstract title</title><link rel="stylesheet" type="text/css" href="/resources/style.css" /></head><body><div align="center"><img src="/resources/banner.jpg" /></div><div align="center" id="box">#abstract content</div></body><script defer type="text/javascript" src="/resources/script.js"></script></html>

?  这个页面是“母版的母版”,包含了基本的html结构,并引用了resources文件夹中的图片、css、js等。其中标注“#abstract”的部分,表示可以被覆盖的部分。我们将在接下来的页面中看到如何进行继承和覆盖:

?  views/Master/home.html

#extends /Master/master#override title博客首页#override content<div id="header">#abstract header</div><h1>Oh, My Simple Blog!</h1>#abstract content

?  这个页面继承了master.html,以“博客首页”覆盖了“#abstract title”,并用一些html片段覆盖了“#content”,不过也引入了新的“#abstract header”和“#abstract content”,又将在继承它的页面中被覆盖。

??

  首页、登录、注册页面

?

  接下来,在views/Home文件夹中增加三个页面:

?  views/Home/index.html?

#extends /Master/home#override header<a href="/User/get/id/${userId}/pageSize/10">我的博客</a><a href="/User/edit/id/${userId}/pageSize/10">博客设置</a><a href="/User/logout">注销登录</a>#override content  

?  这是网站的首页,不过也因为现在还没有做文章显示的部分,所以先用空行覆盖“#abstract content”。

?

  views/Home/login.html

#extends /Master/home#override header<div method="post"><span id="用户名" /></span><span name="password" id="密码" /></span><span name="remember" /></span><span value="登录" /></span><a href="/Home/register">注册新用户</a><a href="/Admin">管理员登录</a></form>#override content

?  这是用户登录页面,包含了一个提交登录信息的表单。

?

  views/Home/register.html

#extends /Master/home#override header<div method="post"><span id="用户名" maxlength="20"  /></span><span name="password" id="密码" maxlength="20" /></span><span id="博客名称" maxlength="20" /></span><span value="注册" /></span><a href="/Home/login">用户登录</a><a href="/Admin">管理员登录</a></form>#override content

  这是用户注册页面,包含了一个提交注册信息的表单。

  现在可以运行网站了(eclipse中的Run as ?-> ?Java Appliaction)。启动网站后,在浏览器中输入http://localhost,可以看到index.html,输入http://localhost/Home/login和http://localhost/Home/register,也可以看到相应的页面。(如果80端口已经被占用,可以到Cfg.cfg中修改port参数)

?

  效果如图:

?

OMTookit引见(1)  简单示例:OMSimpleBlog

?

?

OMTookit引见(1)  简单示例:OMSimpleBlog

?

?

OMTookit引见(1)  简单示例:OMSimpleBlog

?

  实现Home和User中的方法

?

  接下来,我们开始实现后台处理过程。首先在com.omc.entity中新增Home类:

package com.omc.entity;import com.omc.core.*;import com.omc.core.Annotations.*;@Ignorepublic class Home extends Entity {@Role("user")public String index() throws Exception {return toView();}}

  @Ignre表示这个Entity是不会持久化的。@Role("user")表示只有登录后才能访问index页面,否则将重定向到登陆页面。

?

  现在需要修改一下Cfg.cfg,将

loginEntity=HomeloginAction=index

?  这两行,修改为

loginEntity=UserloginAction=checkLogin

?

  接下来,实现User类中的处理方法。

?

  首先,在User类中增加以下属性:

private Session session;private List<Cookie> cookies;private String error = "";private boolean remember;

  这些属性都不会被实例化,只是“视图属性”。

  session和cookies这两个属性将被自动赋值,用于操作session和cookie。error属性用于保存错误信息并显示在页面上。remember属性用于接收 login.html 的“remember(记住我)”参数。

?

  接着,在User中增加 checkLogin() 方法和 checkUser()方法,用于检查用户的登录状态:

public String checkLogin() throws Exception {name.set(Cookie.get(cookies, "name"));password.set(Cookie.get(cookies, "password"));return toView("/Home/" + (checkUser() ? "index" : "login"));}private boolean checkUser() throws Exception {for (Reference<Entity> entity : doList()) {User user = (User) entity.get();if (name.equals(user.name) && password.equals(user.password)) {session.set("role", "user");session.set("userId", user.getId());return true;}}return false;}

?  这些代码将先检查cookie中保存的用户名和密码(简单起见,这里的密码没有加密),然后尝试自动登录,登录失败则导航至 login 页面。现在,再次启动服务器,输入http://localhost,将会被导航至登录页面,而不是index页面。

?

  接着,实现“注册”方法,在User中加入下列代码:

@UpdateOptions(toUpdate = { "name[20]", "password[20]", "blogName[20]" })public String save() throws Exception {if (exists()) {error = cfg("exists") + '\n';return toView("/Home/register");}doSave();session.set("role", "user");session.set("userId", getId());return toView("/Home/index");}private boolean exists() throws Exception {for (Reference<Entity> entity : doList()) {User user = (User) entity.get();if (name.equals(user.name)) {return true;}}return false;}

  这里首先要说明的是@UpdateOptions注解,这个注解有以下作用:

  1. 说明这个方法是中更新的对象将在事务提交时被持久化;对于创建的对象,如果被添加到parent的Children中,也会被持久化。

  2. 在标有这个注解的方法中获取对象时,将同时申请该对象的更新锁,这使得其他线程对该对象的访问需要等待。

  3. 将会对输入URL进行检查,如这里的写法,表示输入URL中包含的Field只能有name、password和blogName,长度最大为20。

  4. @UpdateOptions还有属性 String[] allowEmpty() default {},表示允许为空的输入。

  5.?不符合输入标准的URL将被重定向至登录页面。

?

  总之,这个注解通知OMToolkit准备进行持久化(修改DB),同时也限制了输入,以确保安全。

?

  然后,让我们看看方法中的实现逻辑:

?

  1. 检查用户是否已经存在,如果存在则从配置文件中提取错误信息,记录到error变量中,并返回注册页面。

  为了便于配置错误信息,需要在Cfg文件夹下创建文件User.cfg来记录错误信息:?

exists=该用户名已存在!

??

  2. doSave()方法是继承而来的,表示将自己添加到parent的*List列表中,这里是Database对象的userList列表中。

  由于不会对parent进行持久化,所以parent通常是需要在URL中指定,否则将视为database。

?

  3. 在session中保存role和userId,表示用于已登录,然后显示index页面。

?

  现在可以在此启动服务器,在登录页面中,点击“注册新用户”链接,然后输入用户名、密码、博客名称,点击“注册”按钮,没有问题的话,就会返回index页面了。

?

  “注册”之后,当然就需要“登录”了。在User类中增加登录相关的代码:?

public String login() throws Exception {if (!checkUser()) {error = cfg("error") + '\n';return toView("/Home/login");}if (remember) {cookies.add(new Cookie("name", name.get()));cookies.add(new Cookie("password", password.get()));}return toView("/Home/Index");}

  这段代码表示:先检查用户名、密码是否正确,错误则回到登录页面并显示错误,然后检查“记住我”单选框是否被选中,如果选中则在cookie中保存用户名和密码。同时,还需要在User.cfg中添加一行错误信息:?

error=用户名或密码错误!

  现在启动服务器,用刚刚注册过的用户名登录吧!?

  最后,是“注销登录”,在User类中加入:?

public String logout() throws Exception {session.set("userId", null);session.set("role", null);cookies.add(new Cookie("name", ""));cookies.add(new Cookie("password", ""));return toView("/Home/login");}

?  没什么特别的逻辑,清空session和cookie,回到登录页面。  

???

  “我的博客”与“博客设置”

?

  到目前为止,首页上的“我的博客”与“博客设置”链接都还无法使用。不过由于文章的部分还没有实现,现在能够显示和设置的内容也十分有限。

  首先,增加以下页面:

?  views/master/user.html?

#extends /Master/master#override title${blogName}#override content<div id="header"><a href="/">回到首页</a><a href="/User/get/id/${id}/pageSize/10">文章列表</a></div><h1>${blogName}</h1>${name}#abstract content

??  views/User/get.html?

#extends /Master/user#override content

?  views/User/edit.html?

#extends /Master/user#override content<form action="/User/update" method="post"><input type="hidden" name="id" value="${id}" /><input type="hidden" name="pageIndex" value="${pageIndex}" /><input type="hidden" name="pageSize" value="${pageSize}" />博客名称:<input name="blogName" id="博客名称" maxlength="20" value="${blogName}" /><input type="submit" value="保存" /></form>

??

  在User中增加edit()方法:

@Role("user")public String edit() throws Exception {return toView();}

?  get()方法可以先不加,表示只需要简单地调用toView()就行了。不过后面要显示分页后的文章时,就需要增加get()方法了。

?

  好了,到目前为止,代码应该与附件中的OMSimpleBlog_Step2.rar相似了,运行看看,是否一切正常?遇到问题的话,请看看log.txt中记录的信息,并反馈给我。

?

?

3.文章发布、编辑和删除

?

  光是注册和登录,肯定是不够的。一个blog,至少也要能显示文章吧?接下来我们就实现文章的发布、编辑和删除。

?

  Databse与User的修改

?

  在此之前,让我们将Database类和User类稍作修改。在Database类中加入下列代码:

public void saveArticle(Article article) {articleList.add(article);}public void deleteArticle(long id) {articleList.remove(id);}

?  在User类中增加get()方法,并修改edit()方法:

private List<Reference<Article>> pagedArticles;public String get() throws Exception {pagedArticles = PageUtil.run(articleList, getRequest());return toView();}@Role("user")public String edit() throws Exception {return get();}

  因为我们需要在显示用户的blog的同时显示分页后的文章,所以借助了分页辅助类PageUtil。这个辅助类将按照 id 降序排列文章,由于id的生成是升序的,实际上最新的文章将显示在最前面。

?

?

  保存文章的处理过程

?

  先在Article中加入下列代码:?

private Session session;private interface Getable {public String get() throws Exception;}@Role("user")@UpdateOptions(toUpdate = { "title[100]", "summary[500]", "content[50000]" })public String save() throws Exception {user.set((User) getParent());return checkOwner(new Getable() {public String get() throws Exception {published.set(new Date());doSave();getDatabase().saveArticle(Article.this);return userEditAction();}});}private String checkOwner(Getable afterCheck) throws Exception {Object userId = session.get("userId");if (userId == null || (Long) userId != user.getId()) {getTransaction().rollback();return toView("/Home/login");}return afterCheck.get();}private Database getDatabase() throws Exception {return ((Database) getDatabse());}private String userEditAction() throws Exception {return action("/User/edit/pageSize/10/id/" + user.getId());}

  好的,增加了不少代码。不用着急,让我们一点一点地分析:

?

  首先,Getable这个接口,被用于checkOwner()方法,表示确认当前用户的合法性(防止非法修改别人的博客)之后,真正执行的操作及返回的页面。
  为什么这么做?因为几乎所有对文章的操作都要检查用户的合法性,我们不希望重复这些代码,所以将这个逻辑提取为一个方法。后面将会看到这么做的好处。当然也许有的人不喜欢这么做,也没关系,这里不是重点,基本上属于个人喜好。

?

  然后,是getTransaction().rollback(),这表示事务回滚,取消修改。因为已经确认用户的身份是不合法的了,但对象又已经被更新,所以需要在自动提交之前进行回滚。

?

  最后,让我们看看save()方法的逻辑:先将user属性设为指定的parent(这个parent是通过Web请求中的参数指定的),然后检查用户合法性,接着设置发布日期,通过doSave()将自己保存到parent的articleList中,同时也将自己保存到Database的references中,最后转到User.edit()的调用(action()与toView()不同,将会重新模拟一次请求)。

?

  接着,是一些页面的修改和增加。

?

  显示和发布文章的相关页面

?

  views/Home文件夹下的三个页面文件的最后,都需要增加一行:

#include /Article/list/action/register/pageIndex/${pageIndex}/pageSize/10

?  views/User/get.html,增加:

#if empty没有文章!#end#if !empty#loop pagedArticles<table width="60%" name="code">#if empty没有文章!#end#if !empty#loop pagedArticles<table width="60%" target="_blank">${title}</a><span name="code">#extends /Master/user#override content<form action="/Article/save" method="post"><input type="hidden" name="parent" value="${id}" /><input type="hidden" name="userId" value="${id}" /><input type="hidden" name="author" value="${name}" /><table><tr><td>标题: <input name="title" id="标题" maxlength="100" size="90" /></td></tr><tr><td>摘要:<br /><textarea name="summary" id="摘要" maxlength="500" rows="3" cols="100"></textarea></td></tr><tr><td>正文:<br /><textarea name="content" id="正文" maxlength="50000" rows="15" cols="100"></textarea></td></tr></table><input type="submit" value="保存" /><input type="button" value="取消" onclick="history.back()" /></form>

?  新增views/Master/article.html:

#extends /Master/master#override title${user.blogName}#override content<div id="header"><a href="/">回到首页</a><a href="/User/get/id/${user.id}/pageSize/10">文章列表</a></div><h1>${user.blogName}</h1>#abstract content

??  新增view/Article/list.html:

<table width="60%">#if empty<tr><td colspan="2">没有文章!</td></tr>#end#if !empty#loop<tr><td><div name="code">#extends /Master/article #override content#override content<table width="80%" src="/img/2012/11/20/1719134207.png">

?

?

  接下来,是文章的编辑与删除。

?

  文章编辑与删除

?

  Article中增加:

@Role("user")public String edit() throws Exception {return checkOwner(new Getable() {public String get() throws Exception {return toView();}});}@Role("user")@UpdateOptions(toUpdate = { "title[100]", "summary[500]", "content[50000]" })public String update() throws Exception {return checkOwner(new Getable() {public String get() throws Exception {return userEditAction();}});}@Role("user")@UpdateOptionspublic String delete() throws Exception {return checkOwner(new Getable() {public String get() throws Exception {doDelete();return userEditAction();}});}@Overridepublic void doDelete() throws Exception {user.get().deleteArticle(getId());getDatabase().deleteArticle(getId());}

  edit()方法:检查权限和用户合法性,然后显示编辑页面。

  update()方法:只需加上@UpdateOptions,就会西东保存更新。

  delete()方法:调用doDelete(),返回编辑用户(博客)页面。

  doDelete()方法:将自己从user和database的articleList中移除。

?

  增加页面views/Article/edit.html:

#extends /Master/article#override content<form action="/Article/update" method="post"><input type="hidden" name="id" value="${id}" /><input type="hidden" name="parent" value="${user.id}" /><input type="hidden" name="userId" value="${user.id}" /><input type="hidden" name="author" value="${author}" /><table><tr><td>标题: <input name="title" id="标题" maxlength="100" size="90" value="${title}" /></td></tr><tr><td>摘要:<br /><textarea name="summary" id="摘要" maxlength="500" rows="3" cols="100" >${summary}</textarea></td></tr><tr><td>正文:<br /><textarea name="content" id="正文" maxlength="50000" rows="15" cols="100" >${content}</textarea></td></tr></table><input type="submit" value="保存" /><input type="button" value="取消" onclick="history.back()" /></form>

?

  现在可以测试一下文章编辑和删除功能了。

?

  到目前为止,文章的发布、编辑、删除也完成了,现在的代码应该与附件中的OMSimpleBlog_Step3.rar相似了。

  最后,增加后台数据管理功能。

?

?
  4.后台数据管理

?

  管理页面  

?

  增加views/Admin/login.html:

#extends /Master/master#override title数据库管理员登录#override content<form action="/Admin/admin" method="post"><table><tr><td colspan="2" align="center"><h1>数据库管理员登录</h1></td></tr><tr><td colspan="2" align="center" id="用户名" /></td></tr><tr><td>密码:</td><td><input type="password" name="password" id="密码" /></td></tr><tr><td colspan="2" align="center"><input type="submit" value="登录" /></td></tr></table></form>

?  增加views/Admin/welcome.html:

#extends /Master/admin#override title管理员,您好!#override content<table><tr><td align="center"><h1>管理员,您好!</h1></td></tr><tr><td align="center"><h2>您可以执行如下操作:</h2></td></tr><tr><td><h2><a href="/User/list/pageSize/10">管理用户</a>:查看或删除用户。</h2></td></tr><tr><td><h2><a href="/Article/adminList/pageSize/10">管理文章</a>:查看或删除文章。</h2></td></tr><tr><td><h2><a href="/Admin/logout">注销登录</a>:注销并回到登陆页面。</h2></td></tr></table>

?

  现在可以通过URL http://localhost/Admin/login 和 http://localhost/Admin/welcome 查看这两个页面。

?

  在Admin类中增加:

package com.omc.entity;import com.omc.core.*;import com.omc.core.Annotations.*;import com.omc.server.*;@Ignore@SuppressWarnings("unused")public class Admin extends Entity {private Session session;private String name;private String password;private String error = "";public String index() throws Exception {return toView(isRole("admin") ? "welcome" : "login");}public String admin() throws Exception {if (!name.equals(cfg("name")) || !password.equals(cfg("password"))) {error = cfg("error") + '\n';return toView("login");}session.set("role", "admin");return toView("welcome");}@Role("admin")public String welcome() throws Exception {return toView();}public String logout() throws Exception {session.set("role", null);return toView("login");}}

  index():利用Entity内置的isRole(String role)方法,根据权限跳转至不同页面。

  admin():检查用户名和密码,设置session,跳转至欢迎页面。

  welcome():增加权限控制。

  logout():注销登录,删除session,回到登录页面。

?

  需要新增配置文件 cfg/Admin.cfg:

name=adminpassword=admin1234error=用户名或密码错误!

?

  现在可以测试管理员的登录与注销了。

?

  用户管理

?

  增加页面views/User/list.html:

#extends /Master/admin#override title用户管理#override content<h1>用户管理</h1>#if empty没有用户!#end#if !empty<table width="40%"><tr><th>用户名</th><th>博客名</th><th>操作</th></tr>#loop<tr><td align="center">${name}</td><td align="center">${blogName}</td><td align="center"><a href="/User/delete/id/${id}/pageIndex/${pageIndex}/pageSize/${pageSize}"name="用户">删除</a></td></tr>#end</table>#end#if !empty<a href="/User/list/pageIndex/1/pageSize/${pageSize}">第一页</a>#if hasLastPage<a href="/User/list/pageIndex/${lastPage}/pageSize/${pageSize}">上一页</a>#end#if hasNextPage<a href="/User/list/pageIndex/${nextPage}/pageSize/${pageSize}">下一页</a>#end<a href="/User/list/pageIndex/${pageCount}/pageSize/${pageSize}">最后一页</a>当前第${pageIndex}页,共${pageCount}页#end

?

  在User类中增加:

@Role("admin")public String list() throws Exception {return super.list();}@Role("admin")@UpdateOptionspublic String delete() throws Exception {return super.delete();}

?

  现在可以查看和删除用户了。

  这里需要说明的是,在删除User时,会级联调用Children的doDelete()方法,将相应的文章也删除。

?

?

  文章管理

?

  增加页面views/Article/adminList.html:?

#extends /Master/admin#override title文章管理#override content<h1>文章管理</h1>#if empty没有文章!#end#if !empty<table width="60%"><tr><th>标题</th><th>作者</th><th>发布时间</th><th>操作</th></tr>#loop<tr><td align="center">${title}</td><td align="center">${user.name}</td><td align="center"><span name="code">@Role("admin")public String adminList() throws Exception {return super.list();}@Role("admin")@UpdateOptionspublic String adminDelete() throws Exception {doDelete();return action("adminList");}

?

?

  现在可以查看和删除文章了。

?

  现在的代码,应该与OMSimpleBlog_Complete.rar相似。

  最后,让我们把做好的Blog打包发布吧。

?

?

  打包发布

?

  目前还不能使用Ant功能一键发布,只能手动打包。不过也并不麻烦。

?

  首先,新建文件夹OMSimpleBlog_Release,将工程中的非代码文件复制发到文件夹中。使用eclipse的Export->Jar File功能导出jar,只需勾选src目录,将导出的jar也放在OMSimpleBlog_Release中,编写简单的run.bat文件(linux的话,应该是run.sh):

java -jar server.jar

?

  双击run.bat,启动服务器,看看效果吧!

?

  到目前为止,所有的开发都已经完成了,现在的结果应该与附件中的OMSimpleBlog_Release.rar相似。当然,也可能遗漏了什么。总之,遇到问题的话,尽量反馈给我吧(注意看看log.txt中的错误信息)。谢谢!

1 楼 ericslegend 2011-03-19 很不错啊~

读书人网 >编程

热点推荐