乐者为王

Do one thing, and do it well.

逻辑题-老师的生日

小明和小强都是某老师的学生,老师的生日是M月N日,他们都知道老师的生日是以下十组中的一个。老师把M告诉了小明,把N告诉了小强,然后问他们是否知道自己的生日。小明说“如果我不知道小强也不知道”,小强说“本来我不知道,你说了这话我就知道了”,小明说“那我也知道了 ”。问老师的生日是以下的哪个?

3/4, 3/5, 3/8, 6/4, 6/7, 9/1, 9/5, 12/1, 12/2, 12/8

按M排:

1
2
3
4
3/4, 3/5, 3/8
6/4, 6/7
9/1, 9/5
12/1, 12/2, 12/8

按N排:

1
2
3
4
5
6
9/1, 12/1
12/2
3/4, 6/4
3/5, 9/5
6/7
3/8, 12/8

条件一:小明说“如果我不知道小强也不知道”。分析:因为M都是重复的,所以小明一定不知道。小明知道M以后肯定小强不知道,说明N肯定也是重复,可以剔除12/2和6/7这两个日期。另外,还可以排队N为2与7所对应的月份,因为当老师生日的M为6或12时,小强是有可能知道的,与已知条件相违背。结果:3/4, 3/5, 3/8, 9/1, 9/5。

按M排:

1
2
3/4, 3/5, 3/8
9/1, 9/5

按N排:

1
2
3
4
3/4
3/5, 9/5
3/8
9/1

条件二:小强说“本来我不知道,你说了这话我就知道了”。分析:根据上面的结果和条件二可以知道,现在N一定不能是重复的,可以把3/5和9/5排除。结果:3/4, 3/8, 9/1。

按M排:

1
2
3/4, 3/8
9/1

按N排:

1
2
3
3/4
3/8
9/1

条件三:小明说“那我也知道了”。分析:综合条件二的结果和条件三可以判定现在M应该是单一的,所以只能是9。结果:9/1。

老师的生日为9/1。

.htaccess实例详解

又一次迁移博客服务器。因为是迁移,就没有采取安装的方式,而是把以前的程序拷贝到新服务器上。

打开首页的时候没问题,访问具体文章时就出错了:

1
2
Not Found
The requested URL /sample-article.html was not found on this server.

试着去后台重新做设置,然后就发现在根目录下多了个.htaccess文件,内容如下:

1
2
3
4
5
6
7
8
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

以前也看过这个文件,总是模模糊糊不太明白。今天研究的时候发现一下子就清清楚楚了(这大概就是佛家所说的顿悟吧,不过它的发生应该还是建立在渐修的基础上)。

下面来说说这些内容的理解:

第1行就是打开重写引擎。

第2行是设置URI前缀。

1
2
3
4
5
6
7
8
9
10
Syntax: RewriteRule Pattern Substitution [flags]

- (dash)
 A dash indicates that no substitution should be performed (the existing path
 is passed through untouched). This is used when a flag (see below) needs to
 be applied without changing the path.

last|L
 Stop the rewriting process immediately and don't apply any more rules.
 Especially note caveats for per-directory and .htaccess context.

第3行是完全匹配index.php的URI,但不做替换,并且匹配成功后就停止执行重写过程。

第6行就是把所有的访问请求重写,指给index.php,然后停止执行重写过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Syntax: RewriteCond TestString CondPattern

REQUEST_FILENAME
 The full local filesystem path to the file or script matching the request,
 if this has already been determined by the server at the time REQUEST_FILENAME
 is referenced. Otherwise, such as when used in virtual host context, the same
 value as REQUEST_URI.

You can prefix the pattern string with a '!' character (exclamation mark) to
specify a non-matching pattern.

'-d' (is directory)
 Treats the TestString as a pathname and tests whether or not it exists, and
 is a directory.

'-f' (is regular file)
 Treats the TestString as a pathname and tests whether or not it exists, and
 is a regular file.

第4和第5行是不去测试访问URI是否是目录或文件。其实有了第6行这两行基本就是摆设了。

最后再唠叨一句,有问题直接查官方文档应该是最好的选择。官方文档链接:Apache Module mod_rewrite

逻辑题-100盏电灯

房间里有100盏电灯,编号为1,2,3...100,每盏灯上有一个按钮,初始时灯全都是关的。

编好号的100位同学由房间外依次走进去,将自己编号的倍数的灯的按钮全部按一次,例如第一位同学把编号是1的倍数的灯的按钮按一下(此时100盏灯全亮),第二位同学把编号是2的倍数的灯的按钮按一下(此时只有50盏灯亮着,50盏被这个人按灭了)...第100位同学把编号是100的倍数的灯(即编号为100的灯)的按钮按一下,请问依次走完后,还有多少盏灯亮着?

解题思路:

被按过奇数次的灯亮着,偶数次的灯关了。因为每个同学会把自己编号的倍数的灯全部按一次,所以:

1
2
3
4
5
6
7
8
9
1号灯会被1号同学按下;
2号灯会被1,2号同学按下;
3号灯会被1,3号同学按下;
4号灯会被1,2,4号同学按下;
5号灯会被1,5号同学按下;
6号灯会被1,2,3,6号同学按下;
7号灯会被1,7号同学按下;
...
依此类推

从上面可以总结出n号灯会被以它所有约数为编号的同学按下。

这样,问题就变成了求每盏灯的编号有多少个约数的数学问题。如果约数个数为奇数,灯亮着;反之,灯关着。

Ruby代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Light state: ON/OFF = true/false
lamps = Array.new(100, false)

1.upto(lamps.length) do |lno|
  1.upto(lno) do |fno|
    lamps[lno - 1] = !lamps[lno - 1] if lno % fno == 0
  end
end

lamps.each_index do |i|
  print "Light #{i + 1} is "
  puts lamps[i] ? "ON" : "OFF"
end

最后,我们会发现编号1,4,9,16,25...100的灯是亮着的,其它都是灭的。即编号值可以开出整数平方根的灯是亮的。

我们真的需要接口命名规范吗?

英文原文:http://www.adam-bien.com/roller/abien/entry/in_the_age_of_dryness

译者:感觉这篇文章讲的不错,也不算太长,就翻译了过来。

在过去的项目/审查中我发现了如下的接口及其实现的命名规范:

1
2
3
4
5
IService service = new Service();

Service service = new ServiceImpl();

ServiceIF service = new Service();

每一条都含有多余信息。无论是强调接口或其实现,相关信息都已经包含在代码中,所以都是多余的。这种命名规范仅当你真的不知道如何命名实现的时候才需要,作为去思考一个名称的替代,它更容易依靠现有的模板。

实现名称的缺乏可能也是作为它的非充分责任的一个指标(an indicator for it's unsufficient responsibilities)。因此,如果你找不到一个适当的实现名称,考虑移除接口直接暴露实现可能是个好主意——没有奇怪的规范。实际上JDK中没有这样的规范,接口及其实现也经常称为不同(are often even called differently):

1
2
3
4
5
6
7
Runnable r = new Thread();

RootPaneContainer c = new JFrame();

TableModel t = new DefaultTableModel();

Remote remote = new UnicastRemoteObject();

接口的目的是抽象或从几个实现解耦。一个单一的实现并不需要通过接口来解耦。也有一些例外的规则,如需要使用动态代理等。

通过单个的,有着奇怪命名规范的实现(implementation)来实现(realize)一个接口不应该被认为是一般的最佳实践……

使用Interceptor禁止用户访问未授权的图书信息

现在的JBookShelf有两个问题:

  1. 未登录的用户可以访问图书信息;
  2. 登录后的用户可以访问其他用户的图书信息。

第一个问题可以使用Struts 2的Interceptor来解决:

  1. 创建一个实现了Interceptor接口的类;
  2. 在struts.xml配置中定义这个拦截器;
  3. 在struts.xml中定义一个使用了上面拦截器的拦截栈;
  4. 在struts.xml中定义一个全局转向配置。

实现自己的拦截器有点要注意的是,拦截器必须是无状态的,不要使用在API提供的ActionInvocation之外的任何东西。要求拦截器是无状态的原因是Struts 2不能保证为每一个请求或者action创建一个实例,所以如果拦截器带有状态,会引发并发问题。

创建AuthorizationInterceptor类,继承类AbstractInterceptor。为什么继承它呢?而不是直接实现接口Interceptor。这是因为AbstractInterceptor已经实现了Interceptor接口,并且实现了接口中的init和destroy方法。而在这个拦截器中,我们并不需要使用这两个方法。下面上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.codemany.account.interceptor;

import java.util.Collections;
import java.util.Set;

import com.codemany.account.model.User;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.util.TextParseUtil;

public class AuthorizationInterceptor extends AbstractInterceptor {
    private static final long serialVersionUID = -5140884040684756043L;

    protected Set<String> skipActions = Collections.emptySet();

    public void setSkipActions(String skipActions) {
        this.skipActions = TextParseUtil.commaDelimitedStringToSet(skipActions);
    }

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        User user = (User)invocation.getInvocationContext()
                .getSession().get(User.SESSION_KEY);

        boolean isLogined = user != null;
        String action = invocation.getProxy().getActionName();
        // 如果用户未登录,并且访问的是需要登录权限的Action,就跳转到全局转向配置login上
        if (isLogined || skipActions.contains(action)) {
            return invocation.invoke();
        } else {
            return Action.LOGIN;
        }
    }
}

这里的skipActions目的是为了跳过一些不需要拦截的Action。因为默认情况下,拦截器会拦截Action中的所有的方法。像login,register这类Action是任何用户在任何状态下都可以访问的,所以不需要拦截,这里就可以将这些Action放到skipActions中来跳过拦截。

实现了拦截类后还要在struts.xml进行配置使它起作用,以下是struts.xml的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
    "http://struts.apache.org/dtds/struts-2.3.dtd">

<struts>
    <constant name="struts.enable.DynamicMethodInvocation" value="true" />
    <constant name="struts.devMode" value="true" />

    <package name="default" extends="struts-default">
        <interceptors>
            <interceptor name="authorization"
                class="com.codemany.account.interceptor.AuthorizationInterceptor" />

            <interceptor-stack name="authorizationStack">
                <interceptor-ref name="authorization">
                    <param name="skipActions">login, logout, register</param>
                </interceptor-ref>
                <interceptor-ref name="defaultStack" />
            </interceptor-stack>
        </interceptors>

        <default-interceptor-ref name="authorizationStack" />

        <!-- 全局转向配置 -->
        <!-- 还记得拦截器里面的return Action.LOGIN这句吧,当程序执行完这一行后,-->
        <!-- 就会到struts.xml文件的global-results中找name为login的全局转向配置。-->
        <global-results>
            <result name="login" type="redirectAction">login!input</result>
        </global-results>
    </package>

    <include file="account.xml" />
    <include file="book.xml" />
</struts>

然后将account.xml和book.xml中package继承的父包改成struts.xml配置中的default包:

1
2
3
<package name="account" extends="default">

<package name="book" extends="default">

第二个问题的解决就是修改BookAction.java的代码,不再从数据库中读取Book数据,而是从当前登录用户的books属性中查找Book信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.codemany.book.action;

import java.util.ArrayList;
import java.util.List;

import com.codemany.account.model.User;
import com.codemany.book.model.Book;
import com.codemany.book.service.BookService;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;

public class BookAction extends ActionSupport {
    private static final long serialVersionUID = 2538923417705852774L;

    private Long bookId;
    private Book book;
    private List<Book> bookList;

    private BookService bookService;

    public String list() throws Exception {
        if (bookList == null) {
            bookList = new ArrayList<Book>();
        }
        bookList.addAll(getCurrentUser().getBooks());
        return "list";
    }

    public String show() throws Exception {
        book = getCurrentUser().getBook(bookId);
        return "show";
    }

    public String input() throws Exception {
        if (bookId != null) {
            book = getCurrentUser().getBook(bookId);
        }
        return INPUT;
    }

    public String saveOrUpdate() throws Exception {
        book.setUser(getCurrentUser());
        bookService.saveOrUpdateBook(book);
        return SUCCESS;
    }

    public String delete() throws Exception {
        Book book = getCurrentUser().getBook(bookId);
        if (book != null) {
            bookService.deleteBook(bookId);
            getCurrentUser().getBooks().remove(book);
        }
        return SUCCESS;
    }

    public List<Book> getBookList() {
        return bookList;
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public void setBookId(Long bookId) {
        this.bookId = bookId;
    }

    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

    private User getCurrentUser() {
        return (User)ActionContext.getContext()
                .getSession().get(User.SESSION_KEY);
    }
}

上面的代码会产生异常,提示取出的Book数据为空,所以要在User.hbm.xml中set标签后添加属性lazy=“false”,这样Hibernate从数据库中读取User数据时会连带取出对应的Book数据:

1
<set name="books" inverse="true" lazy="false">

下面是BookAction类中用到的User.getBook(bookId)代码:

1
2
3
4
5
6
7
8
9
10
public class User {

    public Book getBook(Long bookId) {
        for (Book book : books) {
            if (bookId != null && bookId.equals(book.getId())) {
                return book;
            }
        }
        return null;
    }

代码下载:https://github.com/dohkoos/JBookShelf

在Web应用中保存Session数据

保存在客户端

Cookie是一个很好的机制。有时候还可以使用隐藏的表单字段(hidden form fields)。

保存在服务器上

最自然的选择。问题是当不止一台前端服务器时,需要能支持Session复制(session replication)和故障转移(failover)的集群。不管哪个前端服务器收到请求,Session数据都在上面。问题是每次请求时可能都要有数据复制和故障转移的动作,影响性能。有个更简单的方法是Sticky session。即当客户第一次请求时,负载均衡服务器分配一个Cookie给客户,下次请求时就可以通过Cookie访问与上次相同的服务器,这样客户的Session数据就只要在单台服务器上。Sticky session也有两个问题:单点故障和负载均衡可能不是最优。

保存在数据库中

通常最后数据库成为了性能瓶颈。

使用独立的Session存储

可以考虑Memcached或Redis。

实现图书的增删查改(CRUD)

图书的MySQL数据库创建脚本:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE books (
    id int NOT NULL AUTO_INCREMENT,
    title varchar(255) NOT NULL,
    description text,
    image_url varchar(255),
    price decimal(8, 2) NOT NULL,
    author varchar(255) NOT NULL,
    isbn varchar(255) NOT NULL,
    publisher varchar(255) NOT NULL,
    user_id int NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf-8;

图书实体类文件Book.java主要代码:

1
2
3
4
5
6
7
8
9
10
public class Book {
    private Long id;
    private String title;
    private String description;
    private String image_url;
    private double price;
    private String author;
    private String isbn;
    private String publisher;
    private User user;

在User类中添加集合变量books:

1
2
public class User {
    private Set<Book> books = new HashSet<Book>();

在User.hbm.xml中配置和图书的一对多关联:

1
2
3
4
5
<!-- Bidirectional one-to-many association to Book -->
<set name="books" inverse="true">
    <key column="user_id" not-null="true" />
    <one-to-many class="com.codemany.book.model.Book" />
</set>

图书的表映射文件Book.hbm.xml代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<hibernate-mapping>
    <class name="com.codemany.book.model.Book" table="books">
        <id name="id">
            <generator class="increment" />
        </id>

        <property name="title" not-null="true" />
        <property name="description" />
        <property name="image_url" />
        <property name="price" not-null="true" />
        <property name="author" not-null="true" />
        <property name="isbn" not-null="true" />
        <property name="publisher" not-null="true" />

        <many-to-one name="user" column="user_id" not-null="true" />
    </class>
</hibernate-mapping>

BookService.java中的代码相对简单,因为没什么复杂的业务逻辑,只是负责把Action和Dao这两层连接起来。

BookDao.java中的代码主要实现图书的读取,更新和删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.codemany.book.dao;

import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import com.codemany.book.model.Book;

public class BookDao {
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<Book> getBookList() {
        Session session = sessionFactory.openSession();
        try {
            Query query = session.createQuery("from Book b");
            List<Book> bookList = query.list();
            return bookList;
        } finally {
            session.close();
        }
    }

    public Book getBook(Long bookId) {
        Session session = sessionFactory.openSession();
        try {
            Query query = session.createQuery("from Book b where b.id = :id");
            query.setLong("id", bookId);
            query.setMaxResults(1);
            return (Book)query.uniqueResult();
        } finally {
            session.close();
        }
    }

    public void saveOrUpdateBook(Book book) {
        Session session = sessionFactory.openSession();
        Transaction ts = null;
        try {
            ts = session.beginTransaction();
            session.saveOrUpdate(book);
            ts.commit();
        } finally {
            session.close();
        }
    }

    public void deleteBook(Long bookId) {
        Session session = sessionFactory.openSession();
        Transaction ts = null;
        try {
            ts = session.beginTransaction();
            Book book = (Book)session.get(Book.class, bookId);
            session.delete(book);
            ts.commit();
        } finally {
            session.close();
        }
    }
}

BookAction.java负责协调图书应用程序间的运转。接收访问请求,与模型进行交互,将合适的视图展示给用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.codemany.book.action;

import java.util.List;

import com.codemany.account.model.User;
import com.codemany.book.model.Book;
import com.codemany.book.service.BookService;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;

public class BookAction extends ActionSupport {
    private static final long serialVersionUID = 2538923417705852774L;

    private Long bookId;
    private Book book;
    private List<Book> bookList;
    private BookService bookService;

    public String list() throws Exception {
        bookList = bookService.getBookList();
        return "list";
    }

    public String show() throws Exception {
        book = bookService.getBook(bookId);
        return "show";
    }

    public String input() throws Exception {
        if (bookId != null) {
            book = bookService.getBook(bookId);
        }
        return INPUT;
    }

    public String saveOrUpdate() throws Exception {
        User user = (User)ActionContext.getContext().getSession().get("user");
        book.setUser(user);
        bookService.saveOrUpdateBook(book);
        return SUCCESS;
    }

    public String delete() throws Exception {
        bookService.deleteBook(bookId);
        return SUCCESS;
    }

在applicationContext.xml中添加Book的相关配置,如下面代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="mappingResources">
        <list>
            <value>com/codemany/book/model/Book.hbm.xml</value>
        </list>
    </property>
</bean>

<!-- book -->
<bean id="bookDao" class="com.codemany.book.dao.BookDao">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<bean id="bookService" class="com.codemany.book.service.BookService">
    <property name="bookDao" ref="bookDao" />
</bean>

<bean id="bookAction" class="com.codemany.book.action.BookAction" scope="prototype">
    <property name="bookService" ref="bookService" />
</bean>

在Struts 2配置文件struts.xml中include文件book.xml。以下是book.xml文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
    <package name="book" extends="struts-default">
        <action name="*Book" method="{1}" class="bookAction">
            <result name="list">/books/list.jsp</result>
            <result name="show">/books/show.jsp</result>
            <result name="input">/books/form.jsp</result>
            <result type="redirectAction">listBook</result>
        </action>
    </package>
</struts>

视图文件show.jsp作为显示单本图书的信息,代码很简单,就不放出来了,只把list.jsp和form.jsp的代码列出来。

list.jsp用来显示所有的图书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<body>
    <h1>Listing books</h1>

    <table>
        <tr>
            <th>Title</th>
            <th>Description</th>
            <th>Image url</th>
            <th>Price</th>
            <th>Author</th>
            <th>ISBN</th>
            <th>Publisher</th>
            <th>Action</th>
        </tr>

        <s:iterator value="bookList">
        <tr>
            <td><s:property value="title" /></td>
            <td><s:property value="description" /></td>
            <td><s:property value="image_url" /></td>
            <td><s:property value="price" /></td>
            <td><s:property value="author" /></td>
            <td><s:property value="isbn" /></td>
            <td><s:property value="publisher" /></td>
            <td>
                <s:a href="showBook.action?bookId=%{id}">Show</s:a>
                <s:a href="inputBook.action?bookId=%{id}">Edit</s:a>
                <s:a href="deleteBook.action?bookId=%{id}" method="delete">Destroy</s:a>
            </td>
        </tr>
        </s:iterator>
    </table>

    <s:a action="inputBook">Add</s:a>
</body>

当用户新建或者更新图书时,视图form.jsp就上场了。因为被两个逻辑使用,显示的标题等信息也不相同,所以需要判断图书的id是否存在,如果不存在的话即是新建业务,否则就是更新操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<body>
    <s:if test="book.id != null">
        <h1>Editing book</h1>
    </s:if>
    <s:else>
        <h1>Add book</h1>
    </s:else>

    <s:form action="saveOrUpdateBook" method="post">
        <div class="field">
            <s:hidden name="book.id" value="%{book.id}" />
        </div>
        <div class="field">
            <s:textfield name="book.title" label="Title" />
        </div>
        <div class="field">
            <s:textarea name="book.description" label="Description" rows="5" cols="25" />
        </div>
        <div class="field">
            <s:textfield name="book.image_url" label="Image URL" />
        </div>
        <div class="field">
            <s:textfield name="book.price" label="Price" />
        </div>
        <div class="field">
            <s:textfield name="book.author" label="Author" />
        </div>
        <div class="field">
            <s:textfield name="book.isbn" label="ISBN" />
        </div>
        <div class="field">
            <s:textfield name="book.publisher" label="Publisher" />
        </div>
        <div class="actions">
            <s:if test="book.id != null">
            <s:submit value="Update" />
            </s:if>
            <s:else>
            <s:submit value="Save" />
            </s:else>
      </div>
    </s:form>

    <s:if test="book.id != null">
        <s:a href="showBook.action?bookId=%{book.id}">Show</s:a> |
    </s:if>
    <s:a action="listBook">Back</s:a>
</body>

最后还要完成图书模型的服务端校验。Struts 2的校验文件有两种格式:ActionName-validation.xml和ActionName-alias-validation.xml。第一种会对该Action中的每个方法进行校验,不符合只对saveOrUpdate校验的要求。在BookAction.java同目录下创建BookAction-saveOrUpdateBook-validation.xml文件:

1
2
3
4
5
6
7
8
<validators>
    <field name="book">
        <field-validator type="visitor">
            <param name="appendPrefix">true</param>
            <message />
        </field-validator>
    </field>
</validators>

当然,还要在Book.java所在的位置创建Book-validation.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<validators>
    <field name="title">
        <field-validator type="required">
            <message>Price is required</message>
        </field-validator>
    </field>

    <field name="price">
        <field-validator type="required">
            <message>Price is required</message>
        </field-validator>
        <field-validator type="double">
            <param name="minInclusive">0.01</param>
            <message>Price should be at least 0.01</message>
        </field-validator>
    </field>

看着BookAction-saveOrUpdateBook-validation.xml这么长的文件名是不是有点无语,没关系,BookAction-validation.xml配置文件还是可以用的,只要在BookAction.java中那些不需要进行校验的方法上添加@SkipValidation;也可以在action配置中启用validation.excludeMethods参数:

1
2
3
4
<action name="*Book" method="{1}" class="bookAction">
    <interceptor-ref name="defaultStack">
        <param name="validation.excludeMethods">list,show,input,delete</param>
    </interceptor-ref>

代码下载:https://github.com/dohkoos/JBookShelf

Rails中通过Checkbox实现批量删除

在Rails生成的控制器模版中,包含的destroy方法只能处理单个对象,而批量删除要求能够同时处理多个对象,这需要自定义一个批量操作action。批量删除的效果图如下:

每一行记录的第一列设置成Checkbox,用于标记此行是否被选中。表下方放置一个全选Checkbox,表示全部选中或全部反选。全选和反选的JavaScript代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function toggle_checkall(field_name, state) {
  var checkboxes = document.getElementsByTagName('input');
  var count = checkboxes.length;
  for (var i = 0; i < count; i++) {
    if (checkboxes[i].type == "checkbox"
        && checkboxes[i].name == field_name + "_ids[]") {
      checkboxes[i].checked = state;
    }
  }
}
</script>

在routes.rb中配置批量删除action的映射:

1
2
3
resources :departs do
  delete 'destroy_multiple', :on => collection
end

在index.html.erb中添加代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%= form_tag destroy_multiple_departs_path, method: :delete do %>
<%= submit_tag "删除选中" %>
<table>
  <thead>
    <tr>
      <th><input type="checkbox" onclick="toggle_checkall('depart', this.checked);" /></th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><%= check_box_tag "depart_ids[]", depart.id %></td>
    </tr>
    <tr>
      <td><%= check_box_tag "depart_ids[]", depart.id %></td>
    </tr>
  </tbody>
</table>
<% end %>

在控制器中添加批量删除实现代码:

1
2
3
4
5
6
7
8
def destroy_multiple
  Depart.destroy(params[:depart_ids]) unless params[:depart_ids].blank?

  respond_to do |format|
    format.html { redirect_to departs }
    format.json { head :no_content }
  end
end

如何从数组中抽取随机不重复的数字

今天浏览订阅的博客时发现了一个巧妙的从数组中抽取随机不重复数字的算法。譬如,在100个不重复的数字中选择10个不相同的数字,通过这个算法就不需要修改数组长度和删除数组元素等。具体算法如下:

1
2
3
4
5
6
7
8
9
10
11
int[] numbers = new int[100];
int[] selected = new int[10];

for (int i = 0, n = numbers.length; i < selected.length; i++) {
    int idx = (int)(Math.random() * n);    // 随机产生一个从0 - (n-1)的数字
    selected[i] = numbers[idx];
    numbers[idx] = numbers[n - 1];
    n--;    // 减1,从而在下次循环时产生的随机的numbers数组下标的范围从0 - (n-1)-1,
            // 保证了上一步中已经赋值给数组中其它数的numbers[n-1]不会在下次循环中给
            // 取得,确保了产生的数组selected中的数为不重复的。
}

表格内容对齐规则(left、right、center)

  1. 通常情况下内容是左对齐;
  2. 列中的所有数字是整数并且指的是相同事物,或者使用同一个常见单位(公斤,厘米,人数,等等)时,右对齐是一个正确的对齐方式(Table 1);
  3. 如果数字使用相同单位,但混合了整数和小数,通过小数点对齐它们;
  4. 如果数字没有使用同一个常见单位(例如:一个国家的比较中,每个国家都有一个单独的列,每一行包含一个不同的参数——面积,人口,人均收入和预期寿命),左对齐是正确的选择(Table 2);
  5. 居中数字永远都是错的;
  6. 常常会出现列中的一些单元格是空的:例如信息无效或不适用。不管你如何去表示,用dash,n.a.,[?],或其它,也不要让单元格空着,在列中设置符号居中来强调这一事实(Table 2的最后单元格)。