WP-Douban-Post 1.0.0 Released

=== Plugin Name ===
Contributors: windlx
Donate link: http://blog.tech4k.com/
Tags: douban, post, douban-post
Requires at least: 2.0
Tested up to: 3.0.4
Stable tag: 1.0.0

Send one message to your douban broadcasting mysaying when you publish the post.

== Description ==

Send one message to your douban broadcasting mysaying when you publish the post.

== Installation ==

  1. Unzip wp-douban-post to the ‘/wp-content/plugins/’ directory
  2. Activate the plugin through the ‘Plugins’ menu in WordPress
  3. Visit WP Douban Post Options
  4. Click the button “Get Access Token”, it will open a new window for you to authorize WP Douban Post.
  5. Close the window after the authorization is done.
  6. The option page will refresh automatically. If everything works fine, you can see you Douban ID and access token.
  7. Next time when you publish the post, there will be one message sent to your Douban Broadcasting Mysaying.
  8. Any problem, please contact @windlx at Twitter.
  1. 解压后将文件上传到’/wp-content/plugins/’目录
  2. 在插件中激活wp-douban-post
  3. 访问WP Douban Post选项页
  4. 点击Get Access Token按钮, 会打开豆瓣网页,要求你对WP Douban Post应用进行授权。
  5. 授权结束后,点击Close按钮,关闭返回页面。
  6. 选项页会自动刷新,如果一切正常,页面会显示你的Douban ID, Acces Token和Access Token Secret.
  7. 如有问题,请通过新浪微博@windlx联系我

== Frequently Asked Questions ==

= What do I need before the installation? =

You need to have a Douban ID for authorization.

== Screenshots ==

1. WP-Douban-Post.

== Changelog ==

= 1.0.0 =
* Configuration Page in WordPress Admin
* Generate Douban Access Token
* Send one message to user’s Douban Broadcasting Mysaying when publish the post.

== Upgrade Notice ==

= 1.0.0 =
Basic Version.

参考插件
WP-DoubanShow
Douban Connect
Twitter Tools

 

WordPress插件开发入门心得

WordPress用了一段时间,有了一些自己的需求。于是硬着头皮就自己做起了插件。眼见0.1.0总算要release了。把这一周的新的体会记录下来。当作是总结。

插件入门

强烈推荐水煮鱼团队的系列文章(见下面的参考),第一篇非常清楚的描述了Wordpress 插件入门的步骤。其实就是一个附带规定格式备注的php。还能更容易么?

/*
Plugin Name: XXXXXX
Plugin URI: XXXXXXXX
Description: XXXXXXXXXXXXXXXXX
Version: XXXXXXXX
Author: XXXXXXXX
Author URI: XXXXXXXX
*/

把上面的信息保存到php文件中,把php文件放到wp-content/plugins//下。你的插件就可以在Plugins列表中看到了。说实话,这一步超酷的,这也是吸引我做下去的关键一步。

Action and Filter

WordPress提供了丰富的API供开发者调用。其中核心的概念就是Action和Filter。
Action指的就是事件,发布博客,进入Admin页面,激活插件,这些都是action。Wordpress可以让开发者将这些action与自己实现的方法关联起来,从而在事件激发的时候能够完成自己的功能。
Filter指的就是过滤器。Wordpress可以让开发者通过注册filter的方式修改Wordpress地默认表现。例如自定义短链接,格式化标题等等。
参考资料中包含了Action和Filter的具体列表。

后台选项页

没想到如此的简单,三步就能完成。
一,注册Action,用户进入Admin页面的时候需要调用的方法。

add_action('admin_menu', 'plugin_name_options_admin');

二,用户进入Admin页面的时候需要具体显示的文字,注册打开选项页时调用的方法。

//Set option page for the plugin
function plugin_name_options_admin(){
add_options_page('wp_douban_post', __('XXXXXXXX, 'xxxxxxxxxxxxxx'), 5,
__FILE__, 'plugin_name_options');
}

三,实现打开选项页时调用的方法。

function wp_douban_post_options()
{
//页面提交需要实现的功能

if($_POST['update_doubanshow_option'])
{
$wp_doubanshow_option_saved = get_option("wp_doubanshow_option");
$wp_doubanshow_option = array (
"userid" => $_POST['userid'],
"apikey" => $_POST['apikey'],
"profile" => $_POST['profile'],
"collection" => $_POST['collection'],
"recommendations" => $_POST['recommendations'],
"powerby" => $_POST['powerby']
);
if ($wp_doubanshow_option_saved != $wp_doubanshow_option)
{
if(!update_option("wp_doubanshow_option",$wp_doubanshow_option))
{
$message = __('Update Failed', 'wp-doubanshow');
}
}
update_doubanshow();

echo $message ;
}
//这部分是显示的html
<div class=wrap>
<form method="post" action="">
<h2><?php _e('DoubanShow Options', 'wp-doubanshow'); ?></h2>
<fieldset name="wp_basic_options" class="options">
<table>
<tr>
<td valign="top" align="right"><?php _e('Douban ID:', 'wp-doubanshow'); ?></td>
<td><input type="text" name="userid" value="<?php echo $userID; ?>" /> <?php _e('Enter your Douban ID.', 'wp-doubanshow'); ?></td>
</tr>
</table>
</fieldset>
<p class="submit"><input type="submit" name="update_doubanshow_option" value="Update Options »" /></p>
</form>
</div>
}

比我想想的容易很多。轻松过了这关。

参数数据库保存

WordPress提供了get_option()和 c()两个方法,可以方便的保存选项页中的参数。

get_option


<?php echo get_option( $show, $default ); ?>
$show
(string) (required) Name of the option to retrieve. A list of valid default options can be found at the Option Reference.
$default
(mixed) (optional) The default value to return if no value is returned (ie. the option is not in the database).

update_option


<?php update_option( $option_name, $newvalue ); ?>
option_name
(string) (required) Name of the option to update. A list of valid default options to update can be found at the Option Reference.
newvalue
(mixed) (required) The NEW value for this option name. This value can be a string, an array, an object or a serialized value.

需要注意的是所有插件的option都保存在一张表,所以为了避免冲突,option名字需要格外当心。

后台定时任务

WordPress提供了简单机制使得插件可以完成定时更新之类的功能。示例如下。

//自定义Action,并注册相关function
add_action('my_event', 'my_function');

//定义Action的出发频率
if (!wp_next_scheduled('my_event')) {
wp_schedule_event( time(), 'hourly', 'my_event' );
}

//当插件被停止时候,移除相关Function
function update_deactivation(){
wp_clear_scheduled_hook('my_event');
}

register_deactivation_hook(basename(__FILE__),' update_deactivation');

总结

总之就是动手做,其乐无穷啊~~~~~~~~~

参考资料

WordPress Plugin API
Write a Plugin
Plugin API/Action Reference
Plugin API/Filter Reference
自己动手写 WordPress 插件
用 Eclipse PDT 开发一个 WordPress 插件

 

Liferay 6.0.5的定制

安装好了Liferay自然要玩耍一下。不过不管怎么玩都是人家的东西,要最后完成成自己需要的功能和样子,还需要自己去作二次开发定制。关于这方面的内容网上没有比较清晰的思路可以参考,于是自己写了一些方面,给以后留一些参考。

删除sevencogs
默认的安装包中提供了一个叫做sevencogs的示例站点。由于使用了Hooks Plugin,会和原装的liferay表现有所不同,所以如果你自己要做定制的话,最好把他们先删除。
 
rm -rf liferay-portal-6.0.5/tomcat-6.0.26/webapps/sevencogs*

配置Portlets
Liferay除了核心包中提供了很多实用的Portlet之外,还在liferay-portal-6.0.5/tomcat-6.0.26/webapps/下默认部署了很多强大的Portlets。具体的介绍可以参考下面这个页面。
http://www.liferay.com/zh/community/wiki/-/wiki/Main/Liferay+Portlets
个人的建议是不需要的话先删除,反正之后需要了也可以再添加回来。这样服务器启动也可以迅速不少。

配置MYSQL
默认的Liferay使用的嵌入式数据库HSQL,不过你启动的时候Liferay也会提醒你不要太当真,真要用了一定要修改数据库配置。
首先,在mysql中创建数据库。

create database lportal character set utf8;

然后修改下面这个文件(如果没有就创建)。

liferay-portal-6.0.5/tomcat-6.0.26/webapps/ROOT/WEB-INF/classes/portal-ext.properties

在其中添加一下配置(以MYSQL为例)

jdbc.default.driverClassName=com.mysql.jdbc.Driver
jdbc.default.url=jdbc:mysql://localhost/lportal?useUnicode=true&characterEncoding=UTF-8&useFastDateParsing=false
jdbc.default.username=
jdbc.default.password=

之后Liferay启动的时候就会自动读取这个配置文件以对应的数据库配置启动了。如果数据表没有创建过还会贴心的帮你创建并且导入基础数据。
具体各种数据库配置示例可以访问下面的链接。
http://www.liferay.com/zh/community/wiki/-/wiki/Main/Database+Configuration;jsessionid=F70539C702464D413394D1A66428E3EC.node-1

自定义布局和主题
Liferay的外观修改主要分成两个部分Layout和Theme.
Layout指的是布局。页面上每个Portal的大小和相对位置都是通过Layout进行定义的。
Theme指的是主题。页面的图片,HTML风格都是由Theme来决定的。

创建Layout
在最新的Liferay IDE 1.1.0中可以方便的创建Layout Plugin来对这个部分进行定制。
1)创建项目
File ->New Liferay Plug-in Project -> Plug-in Type: Layout.
IDE会创建一个“项目名称-layoutpl”的项目。
2)部署项目
Liferay Server-> Add or Remove -> 选择该项目。启动Liferay -> Server
部署的时候会把项目文件拷贝到liferay-portal-6.0.5/tomcat-6.0.26/webapps/下,然后会被Liferay侦测到,加载到系统中。
3)选择新布局
用管理员帐号登录后,可以通过 Manage -> Page Layout 选择你新开发的布局。

创建Theme
Liferay IDE 1.1.0中同样可以方便的创建Layout Theme来对这个部分进行定制。
1)创建项目
File ->New Liferay Plug-in Project -> Plug-in Type: Layout.
IDE会创建一个“项目名称-theme”的项目。
2)部署项目
Liferay Server-> Add or Remove -> 选择该项目。启动Liferay -> Server
部署的时候会把项目文件拷贝到liferay-portal-6.0.5/tomcat-6.0.26/webapps/下,然后会被Liferay侦测到,加载到系统中。
3)选择新布局
用管理员帐号登录后,可以通过 Manage -> Page -> Look and Feel 选择你新开发的主题。
需要注意的是Liferay支持每个页面配置不同的主题。

创建Portlet
这是大家讨论最多的部分。这边就不细说了。
唯一值得一提的是Liferay开发Portlet支持Plug-in和Ext 两种开发模式。具体的好坏我会以后有机会单独叙述。

为控制面板创建Portlet
除了给每个页面(Page)配置Porlet之外,经常也会有需要为个人控制面板添加Portlet功能。例如我的喜好,我的兴趣,我的足迹之类的功能。
Liferay中控制面板也可以视为一个特殊的页面,左边的菜单栏是一个特殊的Portlet,它会把所有需要显示在控制面板中的Portlet罗列出来。右边则是Portlet点选之后显示的内容。
一个Portlet如果需要显示在控制面板中,需要在/WEB-INF/liferay-portlet.xml中为Portlet添加如下内容

<control-panel-entry-category>my</control-panel-entry-category>
<control-panel-entry-weight>14</control-panel-entry-weight>

control-panel-entry-category指的菜单项的分类,包括’my’, ‘content’, ‘portal’ and ‘server’四种。control-panel-entry-weight指的是菜单项在该分类中的相对位置。
需要注意的是可能会出现常见问题一中的错误。

常见问题
1)Error on line 13 of document : The content of element type “portlet” must match
liferay-portlet.xml对顺序也有一定的要求。最简单的方法就是参考ROOT下的liferay-portlet.xml依样画葫芦,肯定不会错。

参考资料
Liferay THemes.pdf
Liferay Admin Guide

 

liferay 6.0.5 开发环境的搭建

最近在玩liferay,非常赞的一个门户搭建工具。不仅提供了开发工具包,和服务器的捆绑包,论坛和文档的质量也都非常高,最重要地是还有非常多的插件支持。绝对是开发Portlet上佳选择。

记性不好,记下来,说不定啥时候就要用到了。

前提

安装Java JDK 1.5+.

1)下载安装文件

liferay-portal-tomcat-6.0.5.zip捆绑了liferay和tomcat,让你不用省心不少的体贴服务。

liferay-plugins-sdk-6.0.5.zip用于开发liferay plugin的SDK。

liferay-ide-eclipse-updatesite-….zipEclipse 插件,功能强大的liferay plugin开发工具包。

2)在本地解压缩
我个人喜欢把所有相关的安装文件都放在一个目录下。

mkdir liferay
unzip liferay-portal-tomcat-6.0.5.zip
unzip liferay-plugins-sdk-6.0.5.zip

3)开发工具包的安装
使用的是Eclipse 3.5,低版本的请相应查找安装方法。
3.1)安装liferay插件
Install New Software -> Add -> Archive ( 选择liferay-ide-eclipse-updatesite-1.1.0.zip)->逐步安装。
3.2)设置liferay SDK
Preference -> Liferay -> Installed SDKs -> Add (选择liferay-plugins-sdk-6.0.5.zip的解压缩文件夹位置)。
3.3)设置liferay服务器
Preference -> Server -> Runtime -> Add -> Server Type ( Liferay v6.0) -> 选择liferay-portal-tomcat-6.0.5.zip皆压缩位置。
3.4)重启一下。
开发工具包就安装好了。感谢Eclipse强大的插件架构和liferay细心的服务。

4)测试使用
1)添加服务器
在Liferay的Perspective下,Servers -> New Server -> Liferay v6.0.
2)运行服务器
Liferay v6.0 -> Start
3)访问http://localhost:8080/
4)用test@liferay.com/test登录。

使用心得
Liferay Plugin Project 创建的时候会执行一些ant脚本。所以如果你需要使用版本管理的时候,个人建议不要check
in项目控制文件(.project, .classpath),这样你的同伴检出的时候可以通过创建新项目完成这些脚本的执行。避免不同步的现象发生。

参考文章
LifeRay安装配置开发环境全过程(一)
Eclipse – Liferay Wiki
【原创】Liferay Portal二次开发指南(总览)
Liferay Portal 6学习笔记3:Liferay IDE的安装使用

 

基于数据库的Struts Menu,Spring Security整合

Appfuse 2.1.0M1整合了Struts Menu和Spring Security 2.0.4.主要分两个部分实现了权限控制。
Spring Security–用户对权限的控制
1 通过实现Spring Security的一组接口(UserDetails,UserDetailsService, GrantedAuthority …),Appfuse实现了User和Authority之间的关联。用户登录的时候就可以从Database中获取其对应的Authority进行后续的权限判断。
2 通过security.xml, Appfuse定义了Authority对URL,Method,Field的控制。
这其中的一个问题是Authority名称需要hard code在security.xml中,不够灵活。这个问题不在本文的讨论范围内。

Struts Menu–权限对菜单的控制
Struts Menu的menu-config.xml文件定义了菜单内容以及对应的Authority名称。Appfuse通过这样实现了权限对菜单的控制。
这其中的问题是Authority名称需要hard code在menu-config.xml,不能够修改。这也是本文希望解决的问题。

解决思路
同样是Matt的作品,Struts Menu非常的强大灵活。
http://struts-menu.sourceforge.net/security.html
http://demo.raibledesigns.com/struts-menu/dynamicMenu.jsp
Dynamic Menu本身没有实现对权限的控制,但是它提供了一个基本的扩展的思路。

解决办法
Menu类主要基于Dynamic Menu的db schema,增加了与Role(实现了GrantedAuthority接口)的多对多关联,关联方式是Eager,可以一次获得所有的关联关系。

/**
*
*/
package com.shine.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
/**
* @author xiluo2
*
*/
@Entity
@Table(name = "menu")
public class Menu extends BaseObject {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String parentName;
private String title;
private String location;
private Set roles = new HashSet();
/**
* @return the id
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the name
*/
@Column(name="name", length=30)
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the parentName
*/
@Column(name="parent_name", length=30)
public String getParentName() {
return parentName;
}
/**
* @param parentName the parentName to set
*/
public void setParentName(String parentName) {
this.parentName = parentName;
}
/**
* @return the title
*/
@Column(name="title", length=50)
public String getTitle() {
return title;
}
/**
* @param title the title to set
*/
public void setTitle(String title) {
this.title = title;
}
/**
* @return the location
*/
@Column(name="location", length=255)
public String getLocation() {
return location;
}
/**
* @param location the location to set
*/
public void setLocation(String location) {
this.location = location;
}
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "menu_role",
joinColumns = {@JoinColumn(name = "menu_id")},
inverseJoinColumns = @JoinColumn(name = "role_id")
)
public Set getRoles() {
return roles;
}
public void setRoles(Set roles) {
this.roles = roles;
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 0;
result = prime * result + ((location == null) ? 0 : location.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((parentName == null) ? 0 : parentName.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
return result;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (getClass() != obj.getClass())
return false;
Menu other = (Menu) obj;
if (location == null) {
if (other.location != null)
return false;
} else if (!location.equals(other.location))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (parentName == null) {
if (other.parentName != null)
return false;
} else if (!parentName.equals(other.parentName))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
return true;
}
/**
* {@inheritDoc}
*/
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SIMPLE_STYLE)
.append(this.name)
.append(this.title)
.append(this.location)
.toString();
}
}

通过Appfuse可以自动生成对应的MenuDao, MenuManager。
然后就是在系统启动的加载MenuRepository.主要是在StartupListener的setupConext方法中添加下列代码。

MenuManager menuManager = (MenuManager) ctx.getBean("menuManager");
List<Menu> menuList = menuManager.getAllMenus();
MenuRepository repository = new MenuRepository();
repository.setServletContext(context);
MenuDisplayerMapping displayerMapping = new MenuDisplayerMapping();
displayerMapping.setName("Velocity"); displayerMapping.setType("net.sf.navigator.displayer.VelocityMenuDisplayer");
repository.addMenuDisplayerMapping(displayerMapping);
for (int i=0; i < menuList.size() ; i++) {
MenuComponent mc = new MenuComponent();
Menu menuObject = menuList.get(i);
String name = menuObject.getName();
mc.setName(name);
String parent = menuObject.getParentName();
if (parent != null) {
MenuComponent parentMenu = repository.getMenu(parent);
if (parentMenu == null) {
// create a temporary parentMenu
parentMenu = new MenuComponent();
parentMenu.setName(parent);
repository.addMenu(parentMenu);
}
mc.setParent(parentMenu);
}
String title = menuObject.getTitle();
mc.setTitle(title);
String location = menuObject.getLocation();
mc.setLocation(location);
//Generate the role list string
String rolestring = null;
if ( menuObject.getRoles() != null && menuObject.getRoles().size() > 0 ) {
StringBuilder buffer = new StringBuilder();
Iterator iterator = menuObject.getRoles().iterator();
while ( iterator.hasNext() ){
buffer.append(iterator.next().getName());
buffer.append(",");
}
rolestring = buffer.toString();
}
mc.setRoles(rolestring);
repository.addMenu(mc);
}
context.setAttribute(MenuRepository.MENU_REPOSITORY_KEY, repository);

大致的流程和Dynamic Menu一样。主要做了两点修改。
1)将每个Menu对应的Set拼接成字符串,存在MenuComponent中。
2)自行创建MenuDisplayerMapping ,没有利用defaultMenuDisplayerMapping.(主要是StartupListener的时候好像还没有defaultMenuDisplayerMapping,不知道该怎么调整顺序)

OK了。这样User->Role(Authority)->Menu的关联都可以通过数据库方便的操作了。

 

同步你的twitter和开心网记录–基于GAE

一直很烦恼想发的牢骚要在twitter.com和kaixin001.com上说两遍。再有气势的话这么一搞也没啥想法了。
Google App Engine支持Task Queue和Cron Job,简直天生就是做Bot的平台阿。挑了一个周末马上动手搞起来。
从Twitter上抓取消息
我用的是twitter4j.没有特别的比较。觉得还挺方便就用了。不负责推荐。

Twitter twitter = new Twitter("xxx", "xxx");
try {
List list = (List) twitter.getUserTimeline();
for (int i = 0; i < list.size(); i++) { Status status = list.get(i); Query query = pm.newQuery(Tweet.class, "id == myId"); query.declareParameters("long myId"); List results = (List) query.execute(status
.getId());
if (results == null || results.size() == 0) {
Tweet tweet = new Tweet(status.getId(), status.getUser()
.getScreenName(), status.getText());
pm.makePersistent(tweet);
if (tweet.getText().indexOf("#kaixin") > -1) {
Queue queue = QueueFactory.getQueue("sendKaixin");
queue.add(TaskOptions.Builder.url("/sendKaixin")
.method(Method.GET).param("tweetid",
String.valueOf(tweet.getId())));
}
}
}
} catch (TwitterException e) {
log.warning(e.toString());
}

这段Code所在的servlet被配置成Cron Job,定义为每30分钟run一次。
大概的逻辑是说下载自己最新的tweets,如果内容包含”#kaixin”(只是为了方便),就调用TaskQueue的”/sendKaixin”发送到kaixin001上。

向kaixin001.com写消息
kaixin001的网站比较复杂,包含很多ajax,js的东西。看了几个外挂,主要都是对wap.kaixin001.com进行抓取和分析。
wap.kaixin001.com页面非常简单,没有session之类的概念。主要是通过登陆之后html之中带的一个”verifyValue”的值对各个组件进行关联。

URL url = new URL(URL_LOGIN);
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Referer", URL_REFERER);
StringBuilder postData = new StringBuilder().append("email=")
.append(email.replaceAll("@", "%40")).append("&password=")
.append(password).append(
"&from=&login=+%E7%99%BB+%E5%BD%95+");
OutputStreamWriter writer = new OutputStreamWriter(connection
.getOutputStream());
writer.write(postData.toString());
writer.close();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
System.out.println("Kaixin001 login successfully!");
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(),
"UTF-8"));
StringBuilder resultBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
resultBuilder.append(line);
}
reader.close();
String sResult = resultBuilder.toString();
verifyValue = findText(sResult, "/app/mylist.php?verify=",
"\"", 0);
}

有了verifyValue之后只需要向对应的URL提交form表单就可以了。

URL url = new URL(URL_PREFIX_POST_RECORD + verifyValue);
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Referer", URL_PREFIX_WRITE_RECORD
+ verifyValue);
StringBuilder postData = new StringBuilder().append(
"privacy=1&content=").append(URLEncoder.encode(tweet.getText(),"UTF-8"));
OutputStreamWriter writer = new OutputStreamWriter(connection
.getOutputStream());
writer.write(postData.toString());
writer.close();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
tweet.setStatus(Tweet.STATUS_UPDATED);
}
else{
log.warning("Kaixin001 update record failed" + connection.getResponseCode());
}

由于Google App Engine对URL Fetch有一些限制。所以直接用了它提供的URL来做Http Connection.用这个方法提交的记录会直接显示成手机提交,酷阿。
开发中的问题
要做的事情很明确。
1)抓取twitter.
2) 提交kaixin001
3) 更新tweet状态
但是由于Google App Engine中Cron Job和Task Queue都是基于URL Mapping做的,所以每一个请求都有30s的限制,这就对应用提出了要求。一开始考虑的两个方案都会因为超时而失败。
痛定思痛,考虑这两个功能的本质,Cron Job是定时执行,不论成败。Task Queue是定时循环执行,直到成功。
所以最后我的方案是
1)Cron Job定制抓取Twitter,遇到关键字丢给Task Queue.
2) Task Queue循环向kaixin001.com提交,直到成功。

后话
因为涉及twitter.com和kaixin001.com两个网站的账号和密码,所以个人觉得作在线应用会有一些障碍。可能考虑开源让大家自己搭着玩吧。后话了。大家有意见可以给我提。

 

在Google App Engine for Java上使用JSTL

想要在Google App Engine上部署一个Google Apps的Sample Code — Event Publisher,结果问题连连.
一方面是各种dependencies的问题,这个只能怪自己粗心. 另外一个主要的问题就是Event Publisher使用了JSTL而Google App Engine For Java对JSTL的支持有些特别.特地记录一下提醒自己.
主要问题
服务器报错500.

java.lang.AbstractMethodError: javax.servlet.jsp.PageContext.getELContext()Ljavax/el/ELContext;

原因分析
这个错误信息还是非常有名的.Google一下发现大致的原因是getELConext()这个方法是JSTL1.2之后才支持的方法.需要Servlet API 2.5以及JSP API 2.1的支持.具体产生错误的地方包括下面这三个地方.

注意事项一:注意使用的servlet标准版本
web.xml中会标注目前支持的servlet API版本.

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
...
</web-app>

如果你使用了2.4的话,那可能会抱错NoSuchMethodError.本文的错误和这个无关,只是总结一个可能影响JSTL使用的一个因素.

注意事项二:添加jsp header
GAE for Java需要用到下面的jsp header.如果你的jstl在GAE for Java中不工作,那么可以尝试添加下面的语句.

<%@page isELIgnored="false" %>

注意事项三:jstl.jar和standard.jar
Event Publisher需要使用第三方的jstl.jar和standard.jar,所以我在GAE for Java项目中也引入了这两个类库.
但是其实GAE for Java已经提供了对JSTL的支持.而且它的实现和标准的jstl略有差异(标准的jstl.jar不认注意事项二中的tag).所以千万不要画蛇添足自己引入这两个jar文件.

OK,还有什么问题.请留言给我吧.

 

使用Google App Engine的XMPP服务

XMPP作为IM通讯标准协议已经得到了广泛的应用。Sametime, Gtalk 以及Cisco Webex Connect都是基于XMPP协议的IM产品。GAE前一段时间推出了XMPP服务.也许是希望能有更多支持gtalk的服务出现,从而推进gtalk的发展吧.
最近终于有空试用了一下,感觉还不错.想想普通的Web hosting由于缺少独立IP,没有办法实现端口映射,所以基本没有办法用来部署IM机器人。如今有了GAE的XMPP服务,做一个相关的机器人就不只是梦了。如果有同学想在Gtalk,Webex Connect以及其他基于XMPP的IM平台上开发机器人的话,GAE绝对是一个不错的选择.

XMPP消息发送
GAE提供了完整的XMPP类库。你可以在你的web应用中对用户发送XMPP应用或者邀请.示例代码如下.

import com.google.appengine.api.xmpp.JID;
import com.google.appengine.api.xmpp.Message;
import com.google.appengine.api.xmpp.MessageBuilder;
import com.google.appengine.api.xmpp.SendResponse;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
...
String Content = "Hello World";
JID jid = new JID("abc@gmail.com");
Message msg = new MessageBuilder()
.withRecipientJids(jid).withBody(content).build();
XMPPService xmpp = XMPPServiceFactory.getXMPPService();
String result = null;
if (xmpp.getPresence(jid).isAvailable()) {
SendResponse status = xmpp.sendMessage(msg);
if ( status.getStatusMap().get(jid) == SendResponse.Status.SUCCESS){
result = "Success";
}
else{
result = "Failed";
log.info(status.getStatusMap().get(jid).toString());
}
}
else{
xmpp.sendInvitation(jid);
}

XMPP消息接收
XMPP服务支持app-id@appspot.com,anything@app-id.appspotchat.com以及anything@version.latest.app-id.appspotchat.com三种ID,发送到这三种ID的XMPP消息都会自动mapping到下面的URL.该URL默认只能admin才能访问,无须再在web.xml中进行安全配置.

/_ah/xmpp/message/chat/

在web.xml中将此URL映射到你的servlet之后,就可以对发送的XMPP消息进行处理了.

@SuppressWarnings("serial")
public class XMPPReceiverServlet extends HttpServlet {
private static final Logger log = Logger
.getLogger(XMPPReceiverServlet.class.getName());
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException {
XMPPService xmpp = XMPPServiceFactory.getXMPPService();
Message message = xmpp.parseMessage(req);
JID fromJid = message.getFromJid();
String body = message.getBody();
log.info("Receive Message: " + body + " from " + fromJid);
String response = "Echo:"+fromJid.getId();
message = new MessageBuilder().withRecipientJids(fromJid).withBody(
response).build();
xmpp.sendMessage(message);
}
}
}

如上所示,GAE的XMPP提供方便的辅助工具帮你从HTTP参数中获得Message信息.

Message message = xmpp.parseMessage(req);

注意:对GAE XMPP服务ID的添加邀请都会被自动接收,但是邀请无法通知到应用中.

目前XMPP的不足
目前GAE XMPP服务的不足主要包括下面四个方面.

  • 开发与调试不便
  • 无法使用自定义域名
  • 无法判断用户状态
  • 性能限制

开发与调试不便
目前Google App Engine SDK不支持XMPP 服务.只能提供最基本的编译功能.所以如果你想要开发发送或者接收XMPP消息的功能.唯一的办法只有实现编译无误以后上传到Google App Engine上进行测试调试.麻烦可见一斑.

无法使用自定义域名
目前Google App Engine支持的XMPP地址只有@appspot.com和@app-id.appspotchat.com两种.不过考虑到App Engine的Web 服务已经支持了自定义域名.XMPP服务对自定义域名帐号的支持,也是可以期待的一件事情.

无法判断用户状态
目前GAE XMPP服务只能识别对方用户”在线”和”离线”两种状态.作为XMPP特色之一,自定义状态已经被各大IM工具广泛的推广了.尤其是Cisco Webex Connect集成了UC之后添加的”In a Meeting”,”In a Webex Meeting”,”On phone”等状态,让人用的非常舒服.希望GAE XMPP服务的未来版本能对它有所支持,阿门.

性能限制
和普通Web服务一样,GAE的XMPP服务也要受到网络带宽,CPU,内存等各方面的限制.除此之外,对发送的XMPP消息长度也有一定的限制.具体的数据可以参考资料来源中的介绍.皑皑,免费的,还能说啥呢.

OK了,还等什么,做一个属于你自己的XMPP bot吧.
资料来源
Google App Engine, The XMPP Java API

 

墙后的选择:Dabr – Twitter Web End Front

昨晚Twitter神秘被墙-,-,推友们群情激奋,群策群力翻墙推特.Dabr作为一个简单易用的Twitter Web End Front被推友们强力推荐,马上不免俗的安装了一把.

Dabr主页:http://code.google.com/p/dabr/
Dabr安装手册:http://code.google.com/p/dabr/wiki/SetupGuide

系统要求

  • PHP 5.2+
  • curl PHP module
  • mcrypt PHP module
  • mod_rewrite apache module
  • No database is needed

Step 1:下载Dabr
Dabr还是在开发中的版本,只能通过SVN下载.

svn checkout http://dabr.googlecode.com/svn/trunk/ dabr-read-only

Step 2:上传到服务器上


Step 3:配置Dabr
将config.sample.php重命名成config.php,对下列变量进行修改.

  • ENCRYPTION_KEY:用来加密的字符串
  • BASE_URL:一般情况下无须配置
  • FLICKR_API_KEY:不是必选项,如果你用flickr的话才需要配置

OK了,就这么简单,来试试看吧.功能强大,不会被墙.大家一起来翻墙!
我的Dabr:http://twitter.tech4k.com/
更多的Dabr:目前无需翻墙的使用Twitter的方法

 

Adhearsion初体验

我是偶尔在Asterisk: The Future of Telephony上看到Adhearsion的介绍的.以下是摘自Adhearsion主页http://adhearsion.com/的一段对Adhearsion的定义.

Adhearsion is a new way to write voice-enabled applications. It's not just an API or library — it's a fully-featured framework, the first of its kind, designed for maximal code reuse and intuitiveness. The name "Adhearsion" is a combination of "adhesion" and "hear" because Adhearsion shines best when integrating technologies with voice.

简单的说就是基于Asterisk AGI,一套完整的应用于Voice Application的Ruby框架.VoIP Application + Ruby,还有什么比这个更适合我呢?二话不说,马上开始试用.
Adhearsion Getting Start


Adhearsion的主页还是非常的人性化的。不仅提供了完全免费,电话测试系统,使得你建立的Adhearsion应用可以方便的进行调试运行,还提供了多种Voice客户端的选择,包括Skype, 各种SIP Phones甚至In-browser Phone。按照下面的步骤,你可以轻松的建立你的第一个Adhearsion Application.
Step 1:注册Adhearsion帐号
默认会加入Adhearsion的Mailing List,不算很热闹,偶尔会有一些邮件。Adhearsion帐号同时也是电话系统的测试帐号,有了它你就可以连接到Adhearsion提供的SandBox上了.

Step 2:安装Ruby, RubyGems, Adhearsion
Ruby和Gems的安装就不多说了.安装Adhearsion也就是一句话的事情.感谢伟大的Ubuntu和RubyGems.

sudo gem install adhearsion

Step 3:创建你的第一个Adhearsion应用
类似Rails,Adhearsion会创建一整套基础部件,解决了类似Adhearsion服务器启动,停止等工作,使得你可以专心在Adhearsion应用的开发上.

ahn create my_first_app

Step 4:启用Sandbox Component
Adhearsion中有一个Component的概念.Component类似插件的意思,代表了一组功能的集合.所有的Components都放在my_first_app/components目录下.其中Sandbox就是一个包含了连接注册Adhearsion电话测试系统的Component.

cd my_first_app
ahn enable component sandbox

修改my_first_app/components/sandbox/sandbox.yml,按照你在Ahearsion注册的帐号密码修改该文件,这样你无需编写任何代码就可以连接注册到Adhearsion电话测试系统了.

username: xxxxxxx
password: xxxxxxx

Step 5:编写你的Adhearsion应用
打开my_first_app/dialplan.rb,添加如下代码:

sandbox {
play "hello-world"
}

sandbox代表的是Asterisk中规定的context,Adhearsion测试系统默认的context就是sandbox. play “hello-world”的意思就是播放”hello-world”的音频.简单的就好像说话一样-.-


Step 6:运行你的Adhearsion应用
直接通过ahn命令就可以运行你的adhearsion应用了.由于启用了sandbox component,你的应用会先去连接adhearsion测试系统进行登录注册,然后启动后台进程监听请求.

ahn start .



OK了,这就是Adhearsion全部需要做得了.测试一下吧.选一款你觉得最酷的soft phone,按照Ahdearsion的提示拨打电话,听到一声清脆的”Hello World”,你的Adhearsion蹦出一些log信息.看这就爽,你也快来试试看吧.

 

无觅相关文章插件,快速提升流量