package me.test.first.spring.rs.controller; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import me.test.first.spring.rs.entity.ListWrapper; import me.test.first.spring.rs.entity.User; import me.test.first.spring.rs.exception.BusinessException; import me.test.first.spring.rs.http.ContentRange; import me.test.first.spring.rs.http.Range; import me.test.first.spring.rs.http.SortBy; import me.test.first.spring.rs.http.SortBy.Item; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.codec.binary.Base64; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.LastModified; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriTemplate; import org.springframework.web.util.UrlPathHelper; /* * 创建资源: * POST 作用于集合资源, * 比如向 http://test.me/articles/ 使用post新增加一篇文章,则由服务端在该“目录”, * 创建该资源,然后将该资源的URL返回。 * 也即:用户请求的URL是个目录,而返回的新的URL由服务器端确定。 * * 注意:URL模板必须全部一致,比如: * 对于GET请求你使用的URL路径模板如果是 "/student/{studentNum}" 的话, * 对于POST请求你也应该使用相同的路径模板,而不能使用 "/student/{name}", * 否则,在URL处理时会出错。比如若GET请求 "/student/041110108.xml" 的时候, * 期待的studentNum="041110108",但是实际会是 studentNum="041110108.xml"。 * 大家可以带上源代码,DEBUG一下,请在以下两个地方打断点: * AnnotationMethodHandlerAdapter#extractHandlerMethodUriTemplates() * AbstractUrlHandlerMapping#exposeUriTemplateVariables() * * PUT 作用于单个资源 * 比如向 http://test.me/articles/Hello+World 则,用户请求的URL就是会被创建,(不是目录,而是实实在在的资源) */ // http://wenku.baidu.com/view/8f8f2025ccbff121dd36832e.html // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html // http://www.oracle.com/technetwork/articles/javase/index-137171.html // http://www.javahotchocolate.com/tutorials/restful.html // http://www.nbtconsulting.com/rest-web-service-demo/demo-school-outline.html // http://www.jpalace.org/docs/spring/rest.html // http://en.wikipedia.org/wiki/Representational_State_Transfer // http://stackoverflow.com/questions/1601992/spring-3-json-with-mvc // http://code.google.com/p/spring-finance-manager/ /** * * <table border=1 cellspacing=0 cellpadding=0 > * <tr> * <th>URL</th> * <th>HTTP方法</th> * <th>作用</th> * </tr> * <tr> * <td>/user</td> * <td>GET</td> * <td>查询用户列表</td> * </tr> * <tr> * <td>/user</td> * <td>POST</td> * <td>新增用户</td> * </tr> * <tr> * <td>/user/{id}</td> * <td>HEAD</td> * <td>检查资源是否可用</td> * </tr> * <tr> * <td>/user/{id}</td> * <td>GET</td> * <td>查询指定ID的用户信息</td> * </tr> * <tr> * <td>/user/{id}</td> * <td>PUT</td> * <td>更新指定ID的用户信息</td> * </tr> * <tr> * <td>/user/{id}</td> * <td>DELETE</td> * <td>删除指定ID的用户信息</td> * </tr> * </table> * */ @Controller @RequestMapping("/user") public class UserController implements LastModified { private static final Logger logger = LoggerFactory.getLogger(UserController.class); private final Map<Long, User> userMap = new LinkedHashMap<Long, User>(); /** 每条的最后修改时间 */ private final Map<Long, Long> lastModifiedMap = new LinkedHashMap<Long, Long>(); /** 所有记录的最后修改时间,相当于整个表的最后修改时间 */ // when using DB, this should be `SELECT MAX(VERSION) FROM T_USER` private long lastModified = 0; // 未指定分页,或分页后的数据最多能显示的最大数量 private int maxRecords = 15; @Autowired private UrlPathHelper urlPathHelper = null; @Autowired private FileController fileController = null; @PostConstruct public void init() { lastModified = System.currentTimeMillis(); for (User user : genTestData(35)) { userMap.put(user.getId(), user); lastModifiedMap.put(user.getId(), lastModified); } } // 模拟数据库进行查询、排序 private List<User> query(final String name, final SortBy sortBy) { List<User> resultList = new ArrayList<User>(); if (name == null || name.trim().length() == 0) { resultList.addAll(userMap.values()); } else { Iterator<User> it = userMap.values().iterator(); while (it.hasNext()) { User user = it.next(); if (user.getName() != null && user.getName().contains(name)) { resultList.add(user); } } } // sort if (sortBy != null) { Collections.sort(resultList, new UserComparator(sortBy)); } return resultList; } /** * * <ul> * 会返回的状态码:400, 200, 206 * <li> * {@link org.springframework.http.HttpStatus#BAD_REQUEST * HttpStatus.BAD_REQUEST }</li> * <li> * {@link org.springframework.http.HttpStatus#OK * HttpStatus.OK }</li> * <li> * {@link org.springframework.http.HttpStatus#PARTIAL_CONTENT * HttpStatus.PARTIAL_CONTENT }</li> * </ul> */ @RequestMapping(method = RequestMethod.GET) @ResponseBody public ListWrapper list( @RequestHeader(value = "Range", required = false) Range range, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "sortBy", required = false) SortBy sortBy, WebRequest req, HttpServletResponse resp) { logger.debug("SortBy = " + sortBy); if (req.checkNotModified(lastModified)) { return null; } List<User> resultList = query(name, sortBy); List<User> rtnList = resultList; if (range == null) { resp.setStatus(HttpStatus.OK.value()); } else { Integer start = range.getStart(); Integer end = range.getEnd(); if (start > resultList.size() || (end != null && end > resultList.size())) { resp.setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()); resp.setHeader("Content-Range", new ContentRange(0, resultList.size() - 1, resultList.size()).toString()); return null; } if (end >= resultList.size()) { end = resultList.size() - 1; } rtnList = resultList.subList(start, end + 1); resp.setStatus(HttpStatus.PARTIAL_CONTENT.value()); resp.setHeader("Content-Range", new ContentRange(start, end, resultList.size()).toString()); } if (rtnList.size() > maxRecords) { throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "too much records, please using paging."); } ListWrapper data = new ListWrapper(); data.getData().addAll(rtnList); return data; } /** * * 新建一个用户。 * * 注意:新建用户时,一般是通过文件服务异步将头像等文件先传上去,再在表单中进行预览。 * 再提交表单时,就只有包含刚刚上传的文件ID即可。 * * 会返回的状态码: <li> * {@link org.springframework.http.HttpStatus#CREATED * HttpStatus.CREATED }</li> </ul> * * 注意:异步处理时,应当返回 {@link org.springframework.http.HttpStatus#Accepted * HttpStatus.Accepted } 状态码。 * */ @RequestMapping(method = RequestMethod.POST) public void post(@RequestBody User user, HttpServletRequest req, HttpServletResponse resp) { if (user == null) { throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "user info could not be null"); } if (user.getId() != null) { throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "user id is generated at server, could not be specified by client"); } Long newId = 0L; for (User u : userMap.values()) { newId = u.getId() > newId ? u.getId() : newId; } newId++; user.setId(newId); // String uri = urlPathHelper.getRequestUri(req)+"/user/"+newId; String uri = UriComponentsBuilder.newInstance().path("{contextPath}{servletPath}/user/{id}") .build() .expand(urlPathHelper.getContextPath(req), urlPathHelper.getServletPath(req), newId) .encode() .toUriString(); resp.setHeader("Location", uri); resp.setStatus(HttpStatus.CREATED.value()); userMap.put(newId, user); lastModified = System.currentTimeMillis(); lastModifiedMap.put(newId, lastModified); } /** * 获取单个用户信息。 * <ul> * 会返回的状态码: * <li>{@link org.springframework.http.HttpStatus#NOT_FOUND * HttpStatus.NOT_FOUND }</li> * <li> * {@link org.springframework.http.HttpStatus#BAD_REQUEST * HttpStatus.BAD_REQUEST }</li> * <li> * {@link org.springframework.http.HttpStatus#NO_CONTENT * HttpStatus.NO_CONTENT }</li> * </ul> * */ @RequestMapping(value = "/{id}", method = RequestMethod.HEAD) public void head(@PathVariable("id") String idStr, HttpServletResponse resp) { Long id = null; try { id = Long.valueOf(idStr); } catch (NumberFormatException e) { throw new BusinessException(HttpStatus.NOT_FOUND.value(), "user with id =" + id + " not exists"); } if (!userMap.containsKey(id)) { throw new BusinessException(HttpStatus.NOT_FOUND.value(), "user with id =" + id + " not exists"); } resp.setStatus(HttpStatus.NO_CONTENT.value()); } /** * 获取单个用户信息。 * <ul> * 会返回的状态码: * <li>{@link org.springframework.http.HttpStatus#NOT_FOUND * HttpStatus.NOT_FOUND }</li> * <li> * {@link org.springframework.http.HttpStatus#BAD_REQUEST * HttpStatus.BAD_REQUEST }</li> * <li> * {@link org.springframework.http.HttpStatus#OK * HttpStatus.OK }</li> * </ul> * */ @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody public User get(@PathVariable("id") String idStr, WebRequest req, HttpServletResponse resp) { head(idStr, resp); Long id = Long.valueOf(idStr); if (req.checkNotModified(lastModifiedMap.get(id))) { return null; } resp.setStatus(HttpStatus.OK.value()); // 如果请求的path上id的值无法转换为long型,会出发TypeMismathcException而返回400. // 没有合适的消息是否合适? return userMap.get(id); } /** * 更新用户信息。 * 但是不允许设定新ID。 * PUT是完整更新,而不是部分更新。 * TODO 如果有乐观锁(比如时间戳)机制,则需要先判断更新前的锁是否一致,更新完成后还要再更新乐观锁的值。 * FIXME 或者使用If-Match、If-Modified-Since? * * <ul> * 会返回的状态码: * <li>{@link org.springframework.http.HttpStatus#NOT_FOUND * HttpStatus.NOT_FOUND }</li> * <li> * {@link org.springframework.http.HttpStatus#BAD_REQUEST * HttpStatus.BAD_REQUEST }</li> * <li> * {@link org.springframework.http.HttpStatus#NO_CONTENT * HttpStatus.NO_CONTENT }</li> * </ul> * */ @RequestMapping(value = "/{id}", method = RequestMethod.PUT) public void put(@PathVariable("id") String idStr, @RequestBody User newUser, HttpServletResponse resp) { head(idStr, resp); Long id = Long.valueOf(idStr); if (!id.equals(newUser.getId())) { throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "Can not chage user id"); } if (newUser.getHeight() != null && newUser.getHeight() < 0) { throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "height must be positive integer "); } if (newUser.getAvatarId() != null) { // 检查要设置的头像资源是否存在 fileController.head(newUser.getAvatarId().toString(), resp); } // 更新 User user = userMap.get(id); try { PropertyUtils.copyProperties(user, newUser); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new BusinessException(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); } resp.setStatus(HttpStatus.NO_CONTENT.value()); lastModified = System.currentTimeMillis(); lastModifiedMap.put(id, lastModified); } /** * 删除指定的用户。 * * <ul> * 会返回的状态码: * <li> * {@link org.springframework.http.HttpStatus#NOT_FOUND * HttpStatus.NOT_FOUND }</li> * <li> * {@link org.springframework.http.HttpStatus#NO_CONTENT * HttpStatus.NO_CONTENT }</li> * </ul> */ @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public void delete(@PathVariable("id") String idStr, HttpServletResponse resp) { Long id = null; try { id = Long.valueOf(idStr); } catch (NumberFormatException e) { throw new BusinessException(HttpStatus.NOT_FOUND.value(), "user with id =" + id + " not exists"); } if (!userMap.containsKey(id)) { throw new BusinessException(HttpStatus.NOT_FOUND.value(), "user with id =" + id + " not exists"); } userMap.remove(id); lastModified = System.currentTimeMillis(); lastModifiedMap.remove(id); resp.setStatus(HttpStatus.NO_CONTENT.value()); } @Override public long getLastModified(HttpServletRequest request) { // 可以忽略URL参数和HTTP Header // 当它们不同时,是否发送 If-Match、If-Modified-Since 应由浏览器缓存决定。 String mappingUri = urlPathHelper.getPathWithinServletMapping(request); if ("/user".equals(mappingUri)) { return lastModified; } UriTemplate uriTemplate = new UriTemplate("/user/{id}"); if (uriTemplate.matches(mappingUri)) { Map<String, String> pathVarMap = uriTemplate.match(mappingUri); try { Long id = Long.valueOf(pathVarMap.get("id")); if (!lastModifiedMap.containsKey(id)) { return -1; } Long time = lastModifiedMap.get(id); if (time == null) { return -1; } return time; } catch (NumberFormatException e) { return -1; } } logger.warn("request last modifed time for path \"" + mappingUri + "\", not supported, will return -1."); return -1; } public static List<User> genTestData(int count) { List<User> list = new ArrayList<User>(count); for (int i = 1; i <= 35; i++) { User user = new User(); user.setId((long) i); user.setName("zhang3_" + i); // Boolean v = (i / 10) % 3 == 0 ? Boolean.TRUE : (i / 10) % 3 == 1 // ? false : null; if (i % 10 == 0) { user.setGender(null); user.setHeight(null); } else { user.setGender(i % 3 == 1 ? null : i % 3 == 2 ? Boolean.TRUE : Boolean.FALSE); int j = i % 10; user.setHeight(j % 3 != 0 ? Integer.valueOf(180 + j) : null); } user.setAvatarId(1L); user.setBirthday(DateTime.now().withDate(1985, 6, 1).withTime(0, 0, 0, 0).plusDays((int) (i - 1)).toDate()); list.add(user); } return list; } public static void main(String[] args) { List<User> l = genTestData(35); for (User u : l) { System.out.printf("id=%s, height=%d, gender=%s %n", u.getId(), u.getHeight(), u.getGender()); } System.out.println("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"); final SortBy sortBy = SortBy.valueOf("+height,-gender"); Collections.sort(l, new UserComparator(sortBy)); System.out.println("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"); for (User u : l) { System.out.printf("id=%s, height=%d, gender=%s %n", u.getId(), u.getHeight(), u.getGender()); } } } class UserComparator implements Comparator<User> { private SortBy sortBy; public UserComparator(SortBy sortBy) { this.sortBy = sortBy; } @SuppressWarnings({"unchecked", "rawtypes"}) @Override public int compare(User user1, User user2) { List<Item> items = sortBy.getItems(); for (Item item : items) { try { String attr = item.getAttribute(); Comparable v1 = null; Comparable v2 = null; if ("avatar".equals(attr)) { byte[] v = (byte[]) PropertyUtils.getProperty(user1, attr); if (v != null) { v1 = Base64.encodeBase64String(v); } v = (byte[]) PropertyUtils.getProperty(user2, attr); if (v != null) { v2 = Base64.encodeBase64String(v); } } else { v1 = (Comparable) PropertyUtils.getProperty(user1, attr); v2 = (Comparable) PropertyUtils.getProperty(user2, attr); } // null as max value if (v1 != null) { if (v2 == null) { return -1; } else { if (v1.compareTo(v2) != 0) { return v1.compareTo(v2); } } } else { if (v2 != null) { return 1; } } } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } return 0; } }