Tuesday, December 31, 2013

OpenConnect client for Cisco's AnyConnect SSL VPN

If we need to connect Cisco's AnyConnect SSL VPN from an AWS CentOS instance without using Cisco AnyConnect client, there's the OpenConnect as an alternative.

On CentOS 5.4, it's easy to install OpenConnect from enabled Red Hat Enterprise Linux / CentOS Linux Enable EPEL (Extra Packages for Enterprise Linux) repository.
If it is not, then install it:

rpm -ivh http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm

And then install openconnect: yum install openconnect
Then use it to connect VPN: openconnect -u auser cisco-vpn-server
But after enter the password, it throws an error: Failed to open tun device: No such device

Let's check the tun module:

[root@myhost ~]# modprobe tun
FATAL: Module tun not found.

[root@myhost ~]# cat /dev/net/tun
cat: /dev/net/tun: No such device

The module tun is /lib/modules/2.6.16-xenU/kernel/drivers/net/tun.ko. For some reason, if it's missed, so get it from http://s3.amazonaws.com/ec2-downloads/modules-2.6.16-ec2.tgz and unpack/copy entire folder lib/modules/2.6.16-xenU/kernel to /lib/modules/2.6.16-xenU

Then we need to reload modules:
[root@myhost ~]# depmod -ae 2.6.16-xenU
[root@myhost ~]# modprobe tun

Then connect VPN again, and it works: Connected tun0 as a.b.c.d, using SSL
Then just open another terminal and ssh to a server in the network.

Friday, December 13, 2013

Tynamo tapestry-security 0.5.1 and UNAUTHORIZED_URL bug

The Tynamo tapestry security 0.5.1 allows us to configure the URL path when unauthorized access happens. But it only works when we use annotation @RequiresRoles for each page, while it won't if we use Shiro createChain for global configuration, like
, the app displays:

Problem accessing /admin/home. Reason:
/Powered by Jetty:///

instead of configured URL page.

The reason is the org.tynamo.security.shiro.authz.AuthorizationFilter.
getUnauthorizedUrl() overwrites the org.tynamo.security.shiro.AccessControlFilter.getUnauthorizedUrl() where the configured UNAUTHORIZED_URL passed in. The AuthorizationFilter.
getUnauthorizedUrl() returns value from class private variable unauthorizedUrl, so it's always null.

To fix it, just patch AuthorizationFilter by removing unauthorizedUrl, getUnauthorizedUrl(), setUnauthorizedUrl(.) as this.

Tuesday, December 10, 2013

Apache Shiro - Remember me and SerializationException: Unable to deserialze argument byte array

When implementing Tynamo tapestry security 0.5.1, I got exception when using Remember me

[WARN] mgt.DefaultSecurityManager Delegate RememberMeManager instance of type [$RememberMeManager_133e98f5ddc26344] threw an exception during getRememberedPrincipals().
org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array.
at org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)
at org.apache.shiro.mgt.AbstractRememberMeManager.deserialize(AbstractRememberMeManager.java:514)
at org.apache.shiro.mgt.AbstractRememberMeManager.convertBytesToPrincipals(AbstractRememberMeManager.java:431)

Caused by: java.io.StreamCorruptedException: invalid type code: EE
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
at java.io.ObjectInputStream.readArray(ObjectInputStream.java:1639)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1320)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1950)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1874)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1756)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1326)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:348)
at java.util.HashSet.readObject(HashSet.java:291)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:348)
at org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:77)

The Tynamo tapestry security 0.5.1 is based on Apache Shiro 1.2.0

Google around and found it was resolved for 1.2.0 release. That's interesting.

So I tried to debug the ClassResolvingObjectInputStream and ClassUtils and still didn't know the reason why the ClassUtils.forName(..) can't load byte array class. Finally google around for deserialize byte array exception and got JDK-6500212 : (cl) ClassLoader.loadClass() fails when byte array is passed resolved since 2007!

I'm running the app with 1.6.0.jdk on MacOS X so that might cause the problem.

So I patched the ClassUtils as below:

   public static Class forName(String fqcn) throws UnknownClassException {  
     if (clazz == null) {//Luan - patch for byte array class (class name =[B - ref http://bugs.sun.com/view_bug.do?bug_id=6500212  
          try {  
                     clazz = Class.forName(fqcn,false,Thread.currentThread().getContextClassLoader());  
                } catch (ClassNotFoundException e) {  
                     if (log.isTraceEnabled()) {  
              log.trace("Unable to load class named [" + fqcn + "] from the current thread context.");  
     if (clazz == null) {  
       String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +  
           "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";  
       throw new UnknownClassException(msg);  
     return clazz;  

and it worked!

Friday, November 22, 2013

Tapestry 5 https, Tomcat and load balancer plain HTTP proxy

We usually place Tapestry 5 apps running on Tomcat behind an Apache HTTPD with mod_jk / mod_proxy_ajp or mod_proxy_http.

Request protocols are plain HTTP or secure HTTPS from users to the Apache and same thing from Apache to Tomcat. Tapestry 5 auto-detects requests protocol and creates suitable URLs for further calling back actions for the application.

Below are 2 examples:
1. Browsers --http--> Apache --http--> Tomcat --Tapestry 5--http --> Apache --http--> Browsers ...
2. Browsers --https--> Apache --https--> Tomcat --Tapestry 5--https --> Apache --https--> Browsers ...

When we deploy an app required to access via https to load balancers connect to Tomcat in plain http proxy as:
3. Browsers --https--> Load balancers --http--> Tomcat--Tapestry 5 --http--> Load balancers --https --> Browsers ...

Then we get into the browser error “Blocked loading mixed active content” when loading an https page and mix some other http requests in background via AJAX within a page.

The reason is the Tomcat always got http requests from load balancers, so Tapestry 5 builds base URLs in http protocol.

Tapestry 5 allows us to override how these default URLs are created by the BaseURLSource service.

But that's not a good way to go with because we have to decide what protocol should be returned for each deploy environment in code...

Google around and found these posts:

Teaching Tapestry to use network path references
[T5]: BUG: Proxy Situation: Tapestry 5.3.3 Not Respecting isSecure for Form Action URL
Problem pushing application to production

The last one is very helpful when Kalle said about Tomcat connector configuration with proxyPort..

Study more about it and found a solution:

Tomcat http connector should be reconfigured as:
<Connector port="8080" proxyPort="443" secure="true" scheme="https"
protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443"/>

And http proxy in Apache/load balancer:
ProxyPreserveHost On
ProxyPass /T5app http://tomcat:8080/T5app
ProxyPassReverse /T5app http://tomcat:8080/T5app

So the Apache/load balancer handles https come from browsers, but makes http proxy to Tomcat and the Tomcat returns secure https/443 when Tapestry needs to build base URLs.

App web.xml

Saturday, November 16, 2013

Ehcache RMI Replicated Caching in cloud environments

The RMI Ehcache provides 2 ways for peer discovery: Automatic Peer Discovery and Manual Peer Discovery.

If the application is deployed in an environment where multicast is not allowed or not available like Amazon AWS or FireHost clouds, then we must use Manual Peer Discovery.

Follow the guide, configure Manual Peer Discovery as:

Configuration for server1

Configuration for server2


Configuring the CacheManagerPeerListener as:
properties="hostName=server1, port=40001,

properties="hostName=server2, port=40001,

So we need to open the firewall for incoming TCP port 40001 on both peers.

But then each peer server can't send message to other with the connection timed out error:

RMIAsynchronousCacheReplicator Unable to send message to remote peer.  Message was: Connection refused to host: server2; nested exception is:
    java.net.ConnectException: Connection timed out
java.rmi.ConnectException: Connection refused to host: server2; nested exception is:
    java.net.ConnectException: Connection timed out
    at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)
    at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
    at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
    at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:128)
    at net.sf.ehcache.distribution.RMICachePeer_Stub.send(Unknown Source)
    at net.sf.ehcache.distribution.RMIAsynchronousCacheReplicator.writeReplicationQueue(RMIAsynchronousCacheReplicator.java:314)
    at net.sf.ehcache.distribution.RMIAsynchronousCacheReplicator.replicationThreadMain(RMIAsynchronousCacheReplicator.java:127)
    at net.sf.ehcache.distribution.RMIAsynchronousCacheReplicator.access$000(RMIAsynchronousCacheReplicator.java:58)
    at net.sf.ehcache.distribution.RMIAsynchronousCacheReplicator$ReplicationThread.run(RMIAsynchronousCacheReplicator.java:389)
Caused by: java.net.ConnectException: Connection timed out

Review the guide/google around... to find out the problem, but couldn't know why? Then tried to change the firewall to allow all ports and it worked!

So absolutely there was another port that was used by the Ehcache but blocked by the firewall.

Google around and found the answer here
"by default does use portmap and therefore random, non-root ports."

So we need to add remoteObjectPort to the cacheManagerPeerListenerFactory as:
properties="hostName=server1, port=40001, remoteObjectPort=40002,

properties="hostName=server2, port=40001, remoteObjectPort=40002,

And close all ports on the firewall except TCP 40001, 40002 between those peers.

Problem is resolved.

Friday, November 1, 2013

Multiple Tomcat instances on single server and session issue

When setting up 2 instances for Tomcat 6 on the same server, we got the issue of both are sharing sessions when both have same application context name. So if we logged into one instance, the session of the other will be expired.

The reason is cookie is stored by domain and cookie name, but not specified by server port. So both instances use the same JSESSIONID cookie name.

To fix it, we need to change the org.apache.catalina.SESSION_COOKIE_NAME parameter on java command.

We could do that easily with Tomcat bin/catalina.sh by adding that parameter to JAVA_OPTS:

Saturday, October 26, 2013

Install PostgreSQL 8.4 on OS X Mountain Lion

Because a developer needs to use PostgreSQL 8.4 on his new MacBook Pro running OS X Mountain Lion v10.8.3. So I downloaded and installed PostgreSQL as usual.

But when install PostgreSQL 8.4.18 it thrown an error:

Problem running post-install step.
Installation may not complete correctly
The database cluster initialization failed.

Looked into the /tmp/bitrock_installer.log, there's an error:

Preparing to Install
Executing su - postgres -c "\"/Library/PostgreSQL/9.1/bin/pg_ctl\" stop -m fast -w -D \"/Library/PostgreSQL/9.1/data\""
Script exit code: 1

Script output:

Script stderr:
 su: unknown login: postgres

That's weird why it didn't create necessary account.

So I tried to create the postgres user:
# sudo dscl . -create /users/postgres

Then checked it:
# id postgres
id: postgres: no such user

After google around, they said on Lion, the user _postgres has been precreated and PostgreSQL 9 installed.

So let's delete it:

# sudo dscl . -delete /Users/_postgres  

Then installed PostgreSQL 8.4 again, and it worked!

So let's check the psql command path:
# which psql

We need to change to the new place:
# vi ~/.bash_profile
export PATH=/Library/PostgreSQL/8.4/bin:$PATH

# source ~/.bash_profile

Then check it again
# which psql


Tuesday, October 22, 2013

Eclipse jee-europa and Run Jetty Run

Today I setup a new MacBook Pro for a new employee.

After setup Eclipse jee-europa and Run-Jetty-Run, then opened the Run Configurations to create new configuration under the Jetty Webapp, but it didn't create a new configuration as expected. Did nothing and no error at all. Tried with other kinds like Java Application, they worked.

Google around didn't help, so I looked back the Run-Jetty-Run site and tried the old version, then that worked.

Tuesday, September 24, 2013

Multiple SSL Host Headers in IIS

A client needs to setup SSL for both clientdomain.com and www.clientdomain.com for the same website on a IIS 2012 server.

Google around, I found the instruction here.

So we need to create 2 sites: clientdomain.com and www.clientdomain.com where one site (www.clientdomain.com) is configured to redirect request to the other (clientdomain.com) by using the HTTP Redirect module.

To bind the host clientdomain.com, we add site binding with Type: https, IP address: All Unassigned, Port: 443, Host name: clientdomain.com, SSL certificate: (select the SSL certificate)

To bind the host www.clientdomain.com, we need to use the command appcmd:

cd C:\Windows\System32\Inetsrv\
appcmd set site /site.name:"www.clientdomain.com" /+bindings.[protocol='https',bindingInformation='*:443:www.clientdomain.com']

It's done!

Saturday, September 14, 2013

Linux console prompt - weir characters

When login to the server, the console prompt shown up with weir characters (couldn't remember what's caused this)

Äroot§myServer ßÜ#
Äroot§ myServer ßÜ# echo $LANG

Google around, finally found the reset command. That's it :)

Thursday, September 12, 2013

Troubleshoot HTTP 400 Bad Request error on IIS

There's an J2EE app running on the Tomcat with ISAPI redirector on IIS 8 of Windows Server 2012.
Yesterday the client got the error related to an AJAX request. When debugging the problem, I found out that the IIS returns HTTP 400 Bad Request error instead of passing along the url for the Tomcat to handle it.

In the IIS log file, there's no record for this request nor error log neither.

Go to the IIS admin:
Website >
Advanced Settings > Limits > Maximum URL Segments: 32
Request Filtering module > Edit Feature Settings:
Maximum URL Length: 4096
Maximum query string: 2048

Everything is fine. The url that makes error meets those configuration since it has just few segments and the length is around 380 characters - far behind the limit settings.

Then I tried to shorten the url a little bit and then a little bit.. and then finally it worked. It seems there's a limit around 300 characters.

Google around to enable the error log and I found How to troubleshoot HTTP 400 errors
Then go to  Error logging in HTTP APIs
Download the Enable HTTP API error logging  Microsoft Fix it 50634 to install it, but it said the current OS is not matched! Though the instruction applies for Windows Server 2012 Standard as the app is running on.

So have to go to regedit and configure the Http.sys registry settings with following parameters:
EnableErrorLogging: Decimal 1
ErrorLogFileTruncateSize: Decimal 10 (MB)
ErrorLoggingDir: C:\inetpub\logs\LogFiles

Then do : net stop http

It asks to confirm another services also:
   Windows Remote Management (WS-Management)
   Windows Event Collector
   World Wide Web Publishing Service

Then do: net start http
The HTTP Service service was started successfully.

Check the website, it's stopped!
Do  net start http again, but it said the http service is already started!

So go to the services manager to start World Wide Web Publishing Service, then Windows Remote Management (WS-Management) and Windows Event Collector

Then test again and see the error log file with a record just shows a very simple error reason: URL

Continue to read and go to Http.sys registry settings for IIS

Wow, there's a limit setting called UrlSegmentMaxLength with 260 characters by default! I thought that caused the problem because the full url with domain name that worked when there's around 300 characters.

So go to regedit and add UrlSegmentMaxLength with 1000 characters value to the HTTP parameters and restart http, w3svc, winrm and wecsvc services again.

Finally it worked!

I wonder why the MS didn't make it for a each specific website like the Maximum URL Segments and easily to set. But we'll have to configure in the Windows registry that requires to restart whole services and affect all sites.

Monday, September 9, 2013

CVS Change Log for Eclipse

I've been using Eclipse and CVS plugin over 5 years. Sometimes I need to check recently change log of a day but the Eclipse CVS plugin doesn't have that feature and I do that by command line. Today I found a useful plugin and that works great. Because it doesn't provide a link to install directly in the Eclipse, but we can download the zip file and unzip it, then copy the plugins folder to the eclipse folder. There's another issue is the detail dialog doesn't show full file paths as the screenshot from their website. But that's fine.

Update (18 Oct 2013): this version is old and has bug that doesn't show correct logs. The newer version is available on http://code.google.com/a/eclipselabs.org/p/changelog/

Sunday, September 8, 2013

ISAPI Tomcat redirector on IIS 8 of Windows Server 2012 - special chars in URL

When using ISAPI Tomcat redirector on IIS 8 of Windows Server 2012, we faced with the problem of the URL having special chars such as colon (:) character. We had a T5 app that makes AJAX requests and returns IIS error page instead app's JSON data as expected. After debug, I found out that the IIS handles that url (has colon characters) by itself but not lets the ISAPI Tomcat redirector to do that.

The IIS might returns the error like: A potentially dangerous Request.Path value was detected from the client (:).

We might re-factor the app to use normal characters instead colon character but it requires more effort since the function relates to other libraries and have to test all functions...

I tried to place the Tomcat redirector on the top of the ISAPI Filters list or remove all ASP.NET filters, but both ways didn't work either.

Finally, I reconfigured the application pool to remove .NET framework (to No Managed Code) and it worked.

Thursday, September 5, 2013

Yahoo! OpenID attribute exchange

When implementing OpenId authentication with Spring Security with below config, the Yahoo! service doesn't provide any attributes values, although Google works.

      <security:openid-attribute name="email" type="http://schema.openid.net/contact/email" required="true"/>  
      <security:openid-attribute name="forename" type="http://schema.openid.net/namePerson/first" required="true"/>  
      <security:openid-attribute name="surname" type="http://schema.openid.net/namePerson/last" required="true"/>  
      <security:openid-attribute name="name" type="http://schema.openid.net/namePerson/friendly" required="true"/>  

After google around, I found this. So change the config as:

      <security:openid-attribute name="email" type="http://schema.openid.net/contact/email" required="true"/>  
      <security:openid-attribute name="forename" type="http://schema.openid.net/namePerson/first" required="true"/>  
      <security:openid-attribute name="surname" type="http://schema.openid.net/namePerson/last" required="true"/>  
      <security:openid-attribute name="name" type="http://schema.openid.net/namePerson/friendly" required="true"/>  
      <security:openid-attribute name="axFullname" type="http://axschema.org/namePerson" required="true"/>  
      <security:openid-attribute name="axEmail" type="http://axschema.org/contact/email" required="true"/>  

The result is the email attribute has correct value while axEmail and axFullname are null. But if I remove the axEmail, then the email is null. That's interesting. So finally, below config works well with both Google and Yahoo!, though we'll have duplicate email attributes with the same value:

      <security:openid-attribute name="email" type="http://schema.openid.net/contact/email" required="true"/>  
      <security:openid-attribute name="forename" type="http://schema.openid.net/namePerson/first" required="true"/>  
      <security:openid-attribute name="surname" type="http://schema.openid.net/namePerson/last" required="true"/>  
      <security:openid-attribute name="name" type="http://schema.openid.net/namePerson/friendly" required="true"/>  
      <security:openid-attribute name="fullname" type="http://axschema.org/namePerson" required="true"/>  
      <security:openid-attribute name="email" type="http://axschema.org/contact/email" required="true"/>  

Wednesday, August 21, 2013

Implementing Parcelable interface

When looking for a way to implement Parcelable interface, I found a great parcelabler tool by Dallas Gutauckis to generate code quickly.

Suppose we have a class:

 public class DO {  
      private Long id;  
      private String name;  
      private String desc;  
      private Date created;  

Then copy and paste into the textbox and click Build button, the result will be:

 public class DO {  
      private Long id;  
      private String name;  
      private String desc;  
      private Date created;  
   protected DO(Parcel in) {  
     id = in.readLong();  
     name = in.readString();  
     desc = in.readString();  
     long tmpCreated = in.readLong();  
     created = tmpCreated != -1 ? new Date(tmpCreated) : null;  
   public int describeContents() {  
     return 0;  
   public void writeToParcel(Parcel dest, int flags) {  
     dest.writeLong(created != null ? created.getTime() : -1L);  
   public static final Parcelable.Creator<DO> CREATOR = new Parcelable.Creator<DO>() {  
     public DO createFromParcel(Parcel in) {  
       return new DO(in);  
     public DO[] newArray(int size) {  
       return new DO[size];  

Although the options for removing fields won't work, but the tool is useful. Thank you Dallas.

Thursday, August 15, 2013

Custom BaseAdapter and notifyDataSetChanged

When making a custom adapter from BaseAdapter, I got the problem that calling notifyDataSetChanged() doesn't work - new data isn't refreshed as expected.

Below is my code:

 public class RowAdapter extends BaseAdapter {  
   private List<? extends RowDO> data;  
   public RowAdapter(List<? extends RowDO> d) {  
     data = d;  
   public int getCount() {  
     return data.size();  
 public class MainActivity extends Activity {  
   private RowAdapter rowAdapter;  
   private List<RowDO> listDO;  
   private void displayList(){  
     listDO = getListDO();  
     if (rowAdapter == null) {  
       rowAdapter = new RowAdapter(listDO);  
       ListView listView = (ListView) findViewById(R.id.list);  
     } else {  
   private List<RowDO> getListDO() {  
     List<RowDO> list = new ArrayList<RowDO>();  
     list.add(new RowDO());  
     if (some_conditions) {  
     return list;  

After google around, I found Snicolas answer

The list I use in my adapter (data) is a reference to the list created in getListDO() when passing it to adapter at first time. So the next time, when I re-assign the listDO with the new list, the old copy in the adapter is still there.

To correct this, we should keep the original list as below code:

 private List<RowDO> listDO = new ArrayList<RowDO>();  
 private void displayList(){  
   if (rowAdapter == null) {   
     rowAdapter = new RowAdapter(listDO);   
     ListView listView = (ListView) findViewById(R.id.list);   
    } else {   

Tuesday, August 13, 2013

Ruby -=- undefined method `[]' for #Enumerable::Enumerator

Repost from Yann Laviolette blog. I got similar problem and his solution works.

Below is his instruction:

When I upgrade to Ruby 1.8.7 I've got some problems. I'm getting the error message undefined method `[]' for #Enumerable::Enumerator.

The problem occurs with the version 1.8.7 of Ruby and it's supposed to be corrected in the version 9. To fix this problem you can either downgrade to Ruby 1.8.6 OR put this piece of code in your environnement.rb within initializer block:

 unless '1.9'.respond_to?(:force_encoding)  
  String.class_eval do  
    remove_method :chars  
   rescue NameError  
    # OK  

Wednesday, August 7, 2013

SSH restriction users by IPs

To allow or block users SSH to a server, we can use firewall (iptables or AWS Security Groups).
But there are some needs for some specific users. Below is an example to restrict a group of users to SSH from some IPs.

1. Create a group:
groupadd limitgroup
usermod -a -G limitgroup limituser

2. Configure sshd - /etc/pam.d/sshd
account  required  pam_access.so

3. Configure rules - /etc/security/access.conf
-: limitgroup:ALL EXCEPT

Tuesday, August 6, 2013

ActiveAndroid - Model inheritance

I test ActiveAndroid around and found an issue.

When creating a base super DO class that extends from Model class with some common properties/methods, then create some DOs extending from that base DO.

It turns the extended DOs to miss their properties.

Look into more detail, the problem is from the TableInfo.java

 public TableInfo(Class<? extends Model> type) {  
   mType = type;  
   final Table tableAnnotation = type.getAnnotation(Table.class);  
   if (tableAnnotation != null) {  
     mTableName = tableAnnotation.name();  
   else {  
     mTableName = type.getSimpleName();  
   List<Field> fields = new ArrayList<Field>(Arrays.asList(type.getDeclaredFields()));  
   for (Field field : fields) {  
     if (field.isAnnotationPresent(Column.class)) {  
       final Column columnAnnotation = field.getAnnotation(Column.class);  
       mColumnNames.put(field, columnAnnotation.name());  
 private Field getIdField(Class<?> type) {  
   if (type.equals(Model.class)) {  
     try {  
       return type.getDeclaredField("mId");  
     catch (NoSuchFieldException e) {  
       Log.e("Impossible!", e);  
   else if (type.getSuperclass() != null) {  
     return getIdField(type.getSuperclass());  
   return null;  

The reason is the above red codes just get its properties but without including its super classes' properties.

So I created a new method getDeclaredFields to replace the getIdField as below:

 /**Get declared fields from the given class & it's ancestors  
  * @param type class  
  * @return  
 private List<Field> getDeclaredFields(Class<?> type) {  
   List<Field> fields = new ArrayList<Field>();  
   if (type.getSuperclass() != null && type.getSuperclass() != Object.class) {  
   return fields;  

And replace red lines with:
 List<Field> fields = getDeclaredFields(type);  

Android ContentProvider

When an Android app gets into more complex with AsyncTasks to manipulate the SQLite database, we faced with the database locking issues.

So I googled around and found this discussion, then switched to ContentProvider and it solves the problem.

To implement ContentProvider, the first thing to do is adding authorities to AndroidManifest.xml

 <provider android:authorities="com.mycompany.androidapp.contentprovider.authorities"   

The android:authorities="com.mycompany.androidapp.contentprovider.authorities" is just an attribute - whatever unique value - which will be used for the URI to access the data

The class com.mycompany.androidapp.data.ContentProviderDb is extended from android.content.ContentProvider, must implement following required methods:

 public boolean onCreate();  
 public String getType(Uri uri);  
 public Cursor query(Uri uri, String[] projection, String selection,  
                String[] selectionArgs, String sortOrder);  
 public Uri insert(Uri uri, ContentValues values);  
 public int update(Uri uri, ContentValues values, String selection,  
                String[] selectionArgs);  
 public int delete(Uri uri, String selection, String[] selectionArgs);  

Because we need to access the database from different threads, so we must implement the access thread-safe. The simple way is to use synchronized for those query/insert/update/delete methods.

Below is an example

 public class ContentProviderDb extends ContentProvider {  
   private SQLiteOpenHelper openHelper = null;  
    * ContentProvider implementation methods  
    * **************************************/  
   public boolean onCreate() {  
     openHelper = new SQLiteOpenHelper(getContext(), Constant.DATABASE_NAME, null , 1){  
       public void onCreate(SQLiteDatabase db){  
       public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){  
     return true;  
   public String getType(Uri uri) {  
     // TODO Auto-generated method stub  
     return null;  
   /**Execute query: raw query (select/insert/update/delete), create, select data from table  
    * @return Cursor or null  
   public synchronized Cursor query(Uri uri, String[] projection, String selection,  
           String[] selectionArgs, String sortOrder) {  
     SQLiteDatabase database = openHelper.getReadableDatabase();  
     Cursor cursor = null;  
     String query = getQuery(uri);  
     String lcQuery = query.toLowerCase();  
     if (lcQuery.startsWith("select ") || lcQuery.startsWith("insert ") ||   
       lcQuery.startsWith("update ") || lcQuery.startsWith("delete ") ) { //raw queries  
       cursor = database.rawQuery(query, null);  
     } else if (lcQuery.startsWith("create ")) { //create tables  
     } else { //query is a table name -> normal select from table  
       cursor = database.query(query, projection, selection, selectionArgs, null, null, sortOrder);  
     return cursor;  
   public synchronized Uri insert(Uri uri, ContentValues values) {  
     String table = getQuery(uri);  
     SQLiteDatabase database = openHelper.getWritableDatabase();  
     long insertedId = database.insert(table, null, values);  
     return Uri.parse("content://" + getAuthority() + "/" + table + "/" + insertedId);   
   public synchronized int update(Uri uri, ContentValues values, String selection,  
           String[] selectionArgs) {  
     String table = getQuery(uri);  
     SQLiteDatabase database = openHelper.getWritableDatabase();  
     return database.update(table, values, selection, selectionArgs);  
   public synchronized int delete(Uri uri, String selection, String[] selectionArgs) {  
     String table = getQuery(uri);  
     SQLiteDatabase database = openHelper.getWritableDatabase();  
     return database.delete(table, selection, selectionArgs);  
    * ContentProvider implementation methods - END  
    * *********************************************/  
   /**Get authority  
    * @return authority string  
   private String getAuthority() {  
     return "com.mycompany.androidapp.contentprovider.authorities";  
   /**Extract query from uri  
    * @param uri  
    * @return extracted query/table name  
   private String getQuery(Uri uri){  
     return uri.getPath().replace("/", "");//remove '/'  

To create tables and populate the database, we can either add some methods to onCreate of SQLiteOpenHelper in onCreate of ContentProviderDb:

 openHelper = new SQLiteOpenHelper(getContext(), Constant.DATABASE_NAME, null , 1){  
   public void onCreate(SQLiteDatabase db){  
     createTables(); //your own implementation 
     autoPopulate(); //your own implementation 

Or in the activity (with copy from existing database):

 private String database_path = "";  
 if(android.os.Build.VERSION.SDK_INT >= 4.2){  
   database_path = getBaseContext().getApplicationInfo().dataDir + "/databases/";   
 } else {  
   database_path = "/data/data/" + getBaseContext().getPackageName() + "/databases/";  
 if(!existDataBase()) {  
   try {  
     if (!copyDataBase()) {  
       getContext().getContentResolver();//init the ContentProviderDb to create the database  
   } catch (IOException e) {  
       Log.e("IOException", "exception in copyDataBase() method");  
 /**Check if database exist  
  * @return true if the database is existed, false if not  
 private boolean existDataBase() {  
   File dbFile = new File(getDatabasePath());  
   return dbFile.exists();  
 /**Get database path  
  * @return path string  
 private String getDatabasePath() {  
   return database_path + Constant.DATABASE_NAME;  
 /**Copy the existing database from the data directory  
  * @return true if the database is copied, false if not  
  * @throws IOException  
 private boolean copyDataBase() throws IOException {         
   File dbFile = getSourceDatabaseFile();// your own implementation
   if (dbFile.exists()) { //copy the database  
     InputStream in = new FileInputStream(dbFile);  
     OutputStream out = new FileOutputStream(getDatabasePath());  
     byte[] mBuffer = new byte[1024];  
     int mLength;  
     while ((mLength = in.read(mBuffer))>0) {  
       out.write(mBuffer, 0, mLength);  
     in = null;  
     out = null;  
     return true;  
   return false;  

To manipulate the data in the activity, we use ContentResolver instead ContentProvider.

  * Creates tables  
 private void createTables(){                                
     for(String sql : getTableCreationSQLs()){  
         execSQL(sql);  //query starts with "create table "
 /**Used for executing query, start with create (not select/insert/delete/update)  
  * @param query  
 private void execSQL(String query) {  
     query(query, null, null, null);  
 /**Execute query or select data from table  
  * @param query : query or table name  
  * @param columns  
  * @param whereClause  
  * @param orderBy  
  * @return cursor  
 private Cursor query(String query, String[] columns, String whereClause, String orderBy){  
   return getBaseContext().getContentResolver().query(getUri(query), columns, whereClause, null, orderBy);  
 private Uri getUri(String query){  
   return Uri.parse("content://" + getAuthority() + "/" + query);  

Monday, August 5, 2013

Apache HTTPD, Tomcat configuration and Tapestry 5 app

A client wants to run their Tapestry 5 app as root - i.e http://clientdomain.com/ - instead http://clientdomain.com/clientT5app/

So lets configure the Apache HTTPD

 <VirtualHost *:80>  
     ServerName clientdomain  
     ProxyPass / ajp://localhost:8009/  

And Tomcat server.xml configuration:

 <Host name="localhost" appBase="root"  
       unpackWARs="true" autoDeploy="true"  
       xmlValidation="false" xmlNamespaceAware="false">  
     <Context path="/manager" debug="0" privileged="true" docBase="/var/lib/tomcat6/webapps/manager">  
         <ResourceLink name="users" global="UserDatabase" type="org.apache.catalina.UserDatabase"/>  

We need to change appBase to another folder (like /var/lib/tomcat6/root) or move clientT5app out of webapps folder in case appBase="webapps" as default to avoid double loadding the clientT5app.

The context manager is used to manage applications.

Sunday, August 4, 2013

Rails 2 debug logger

I need to have clean log for a Rails 2 app. So follow the guide to change the environment.rb:

Rails::Initializer.run do |config|

config.log_level = :debug

From myController:
logger.debug "Debug some thing"

But it didn't work. There's no log in the development.log!

Then switch to 
config.log_level = :warn

From myController:
logger.warn "This works!"

Worked! Don't know why? But that's it.

Saturday, July 27, 2013

Rails helpers

I have a task to fix a conversion bug in a Rails app (2.12), then I found out that there are copies of the same method in everywhere - that's so bad. So I decided to clean it up by making a common helper that provides common methods for global usage, like below example:

 module UtilsHelper  
  def convertValue(value)  
   return "0" if value.nil?  
   return (value*2).to_s  

Google around, finally get it to run:

In the Controller, use helper method to include UtilsHelper to use in the template:
 class HomeController < ApplicationController  
  helper UtilsHelper #include won't work  

index.rhtml template:
 <%= convertValue(row.value) %>  

But in a class, we must use include method
 class Data  
  include UtilsHelper #require won't work  
  @value = nil  
  def to_s  
   return convertValue(@value)  

Thursday, July 25, 2013

Installing MySQL and gem on Snow Leopard 10.6.8

When I start an old Rails app with Ruby 1.8.7, Rails 2.1.2 on my MacBook laptop, I got error with MySQL connection, check the log:

WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. Please install the C-based MySQL library instead (gem install mysql).

Before that, I had installed mysql-5.5.21-osx10.6-x86.dmg
I tried several ways to fix that like gem install mysql2 or sudo env ARCHFLAGS="-arch i386" gem install mysql --with-mysql-dir=/usr/local/mysql --with-mysql-lib=/usr/local/mysql/lib   --with-mysql-include=/usr/local/mysql/include  --with-mysql-config=/usr/local/mysql/bin/mysql_config

But it still didn't work. So I decided to remove MySQL and install it from source as others suggested.

Remove MySQL
- First, backup databases.
- Stop MySQL (then check & kill exist MySQL processes)
- Follow instructions
sudo rm /usr/local/mysql
sudo rm -rf /usr/local/mysql*
sudo rm -rf /Library/StartupItems/MySQLCOM
sudo rm -rf /Library/PreferencePanes/My*
edit /etc/hostconfig and remove the line MYSQLCOM=-YES-
rm -rf ~/Library/PreferencePanes/My*
sudo rm -rf /Library/Receipts/mysql*
sudo rm -rf /Library/Receipts/MySQL*
sudo rm -rf /private/var/db/receipts/*mysql*

Download & install MySQL
Follow instructions
(Note that the mysql 5.5 use cmake to configure, so just need 5.1 for now)

cd ~/Downloads
curl -O http://mysql.mirrors.pair.com/Downloads/MySQL-5.1/mysql-5.1.70.tar.gz
tar xvzf mysql-5.1.70.tar.gz
cd mysql-5.1.70
./configure --prefix=/usr/local/mysql --with-extra-charsets=complex \
--enable-thread-safe-client --enable-local-infile --enable-shared \
sudo make install
cd /usr/local/mysql
sudo chown -R mysql ./var

sudo ./bin/mysql_install_db --user=mysql

Installing MySQL system tables...
ERROR: 1004  Can't create file '/var/tmp/#sql1599d_1_0.frm' (errno: 9)
130725  8:00:57 [ERROR] Aborting

130725  8:00:57 [Note] /usr/local/mysql/libexec/mysqld: Shutdown complete

#fix error
#clean up data, must! - if not, then the mysql table is still empty, can't login as root
sudo rm -Rf var
#create tmp dir
sudo mkdir -p var/tmp
sudo chown -R mysql var
sudo ./bin/mysql_install_db --user=mysql --tmpdir=/usr/local/mysql/var/tmp
sudo /usr/local/mysql/libexec/mysqld --skip-grant --user=mysql
mysql -uroot
mysql > use mysql;
mysql > select * from user;
mysql > exit)

sudo /usr/local/mysql/bin/mysqladmin -u root shutdown

ps aux | grep mysql
kill all exist mysql processes

sudo /usr/local/mysql/bin/mysqld_safe --user=mysql &

sudo /usr/local/mysql/bin/mysqladmin -u root password 'rootXYZ0'

sudo /usr/local/mysql/bin/mysqladmin -u root -p shutdown

Starting (and Auto-Starting) MySQL

Because the com.mysql.mysqld.plist link from hivelogic.com died, so I found another

echo "<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
</plist>" >> ~/com.mysql.mysqld.plist

sudo mv ~/com.mysql.mysqld.plist /Library/LaunchDaemons/com.mysql.mysqld.plist

sudo chown root /Library/LaunchDaemons/com.mysql.mysqld.plist

sudo launchctl load -w /Library/LaunchDaemons/com.mysql.mysqld.plist

sudo launchctl unload -w /Library/LaunchDaemons/com.mysql.mysqld.plist)

Gem install
gem list
gem uninstall mysql
env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config

Note: don't use sudo env ARCHFLAGS="-arch i386" gem install mysql... That will not work

Add path/alias to ~/.bash_profile
export PATH=$PATH:/usr/local/mysql/bin
export DYLD_LIBRARY_PATH=/usr/local/mysql/lib:$DYLD_LIBRARY_PATH
alias mysqlstart="sudo launchctl load -w /Library/LaunchDaemons/com.mysql.mysqld.plist"
alias mysqlstop="sudo launchctl unload -w /Library/LaunchDaemons/com.mysql.mysqld.plist"
alias mysqlstatus="ps aux | grep mysql | grep -v grep"

Launch app
cd ~/rails/myapp
ruby script/server

Wednesday, July 24, 2013

Horizontal List View with scrollbar

To display list of thumbnail images horizontally on the Android, two images per screen, we use Horizontal ListView from DevsmartLib, but the client would like to show up horizontal scrollbar and position indicator also.

Because the lib doesn't support those features, so we must implement them.

To display the position indicator when scrolling, I add function displayPosition() at the bottom of the onLayout() method:

 protected synchronized void onLayout(boolean changed, int left, int top,    
       int right, int bottom) {   
 private int childWidth=0;  
 private void displayPosition() {   
      String counter = "";  
      if (getChildCount()>0 && mAdapter.getCount()>2) {  
           if (childWidth==0) {  
                childWidth = getChildAt(0).getMeasuredWidth();  
           double positionX = (double)mCurrentX/childWidth;  
           double fractional = positionX % 1;  
           int position = 1 + (int)positionX;  
           if (fractional > 0.3) position++;  
           counter = String.format(" - %s/%s",position,mAdapter.getCount());  
      ((Activity) this.getContext()).setTitle("List"+counter);  
To display horizontal scrollbar, it's not easy because it's hard to find examples or good reference, have to google a lot!

Finally found out this, so just implement necessary functions:

 public HorizontalListView(Context context, AttributeSet attrs) {   
   TypedArray a = context.obtainStyledAttributes(R.styleable.View);   
 protected int computeHorizontalScrollOffset() {   
      return this.mCurrentX;   
 protected int computeHorizontalScrollRange() {  
      return mAdapter.getCount()<3?0:(mAdapter.getCount()*childWidth);  

The shorten attrs.xml as below:

 <?xml version="1.0" encoding="utf-8"?>  
 <declare-styleable name="View">  
 <attr name="android:scrollX"/>  
 <attr name="android:scrollY"/>  
 <attr name="android:scrollbarAlwaysDrawHorizontalTrack"/>  
 <attr name="android:scrollbarAlwaysDrawVerticalTrack"/>  
 <attr name="android:scrollbarDefaultDelayBeforeFade"/>  
 <attr name="android:scrollbarFadeDuration"/>  
 <attr name="android:scrollbarSize"/>  
 <attr name="android:scrollbarStyle"/>  
 <attr name="android:scrollbarThumbHorizontal"/>  
 <attr name="android:scrollbarThumbVertical"/>  
 <attr name="android:scrollbarTrackHorizontal"/>  
 <attr name="android:scrollbarTrackVertical"/>  
 <attr name="android:scrollbars"/>    

Note: It's nice to view code in blogger with codeformatter :). Thank you.

Saturday, July 20, 2013

Calculate Google Map zoom level on Android

On Android, Google Map API doesn't have function to export the map to image file directly. But the Google provides Static Maps API to render map into image via http request. An issue comes up when we need to show all locations on the map, so we have to supply the zoom level parameter.

If the map is rendering on the GoogleMap, it's easy to get the center and zoom of the boundary with the code:

 final LatLngBounds.Builder builder = new LatLngBounds.Builder();  
 for (LatLng latlng : list) {  
 mMap.setOnCameraChangeListener(new OnCameraChangeListener() {  
   public void onCameraChange(CameraPosition arg0) {  
     // Move camera.  
     mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 10));  
     // Remove listener to prevent position reset on camera move.  
     LatLng center = mMap.getCameraPosition().target;  
     float zoom = mMap.getCameraPosition().zoom;  

But when we need to export by batch or from the server side, we won't load each map data onto the map view to execute above code. So I find a way to calculate them by formula.

The center point is simple to do:

 final LatLngBounds bounds = builder.build();  
 LatLng ne = bounds.northeast;  
 LatLng sw = bounds.southwest;  
 LatLng center = new LatLng((ne.latitude + sw.latitude)/2,  
             (ne.longitude + sw.longitude)/2);  

There are several ways to calculate the zoom level, but I find this code in Javascript by John S is more accurate (I convert them into java code)

 public static int getBoundsZoomLevel(LatLng northeast,LatLng southwest,   
                   int width, int height) {  
   final int GLOBE_WIDTH = 256; // a constant in Google's map projection  
   final int ZOOM_MAX = 21;  
   double latFraction = (latRad(northeast.latitude) - latRad(southwest.latitude)) / Math.PI;  
   double lngDiff = northeast.longitude - southwest.longitude;  
   double lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;  
   double latZoom = zoom(height, GLOBE_WIDTH, latFraction);  
   double lngZoom = zoom(width, GLOBE_WIDTH, lngFraction);  
   double zoom = Math.min(Math.min(latZoom, lngZoom),ZOOM_MAX);  
   return (int)(zoom);  
 private static double latRad(double lat) {  
   double sin = Math.sin(lat * Math.PI / 180);  
   double radX2 = Math.log((1 + sin) / (1 - sin)) / 2;  
   return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;  
 private static double zoom(double mapPx, double worldPx, double fraction) {  
   final double LN2 = .693147180559945309417;  
   return (Math.log(mapPx / worldPx / fraction) / LN2);  

Wednesday, July 17, 2013

Network Solutions down

Got up early in the morning, then checked my company emails on the iPhone and got connection error, it's around 6:15 AM. Didn't get any Nagios notification emails about the mail server problem. Open the laptop and can't SSH to the mail server and Nagios server either, that's weird. nslookup them, could not resolve IPs! Login into the Network Solutions which hosts our domain names, it was hanging forever, seems to be down. Got back to SSH to servers via IP addresses, they worked. That's why the Nagios was still working well. So checked around other name servers, some still cached while other not.

Went to the office at 8 and found just mail works with one of our Comcast DNS server,, other hosts came up lately. The http://networksolutions.com was completely down. It just came back up around 10:30. Some DNS servers like Comcast, Google, still can't query our hosts.

Find out solution for this issue in the future. We'll definitely need secondary name servers from other providers!

Here is additional information about the Network Solutions's problem:

Tuesday, July 16, 2013

ISAPI Tomcat redirector on IIS 8 of Windows Server 2012

A client wants to migrate a J2EE app running on Tomcat and IIS 6 of Windows 2003 to IIS 8 of Windows Server 2012.

This is the first time I have a chance to work with Windows Server 2012!

After login via RDP I noticed the GUI looks like Windows 8 as I had a little bit experience to play with it when buying and setup an Asus touchscreen laptop for my brother.

I don't have any problems to copy files between the old server to this server and setup Java, Tomcat, MySQL, PostgreSQL, PostGIS... Except when start the Tomcat 5.5, it throws errors because the Java is 64 bit, so download Java 32 bit and reinstall it.

And to start Tomcat, we need to copy %JAVA_HOME%\bin\msvcr71.dll to Tomcat\bin folder.

Another issue is setup the Tomcat to trust SSL certificate from a https website that the J2EE app has request connection to it. Just copy the old jssecacerts file to "%JAVA_HOME%\lib\security\

But I want to test it by the command line first, just hit the shortcut Windows PowerShell on the launch bar, it open the console window looks like the MS-Dos window, except blue background, then execute command:
java -Djavax.net.ssl.trustStore="%JAVA_HOME%\lib\security\jssecacerts" myTest

And got the error:

Exception in thread "main" java.lang.NoClassDefFoundError:/net/ssl/trustStore=%JAVA_HOME%\lib\security\jssecacerts
Caused by: java.lang.ClassNotFoundException: .net.ssl.trustStore=%JAVA_HOME%\lib\security\jssecacerts

Don't know why, check/test everything is fine, the command run well on the old server. Try to add class path for the net/ssl.... Didn't work! Crazy about this.

Finally I thought the Windows PowerShell is different MS-Dos, so try to find a way to exec cmd. Don't know where the MS-Dos shortcut is, have to google again. Then Search > Run, cmd, enter the above command, it worked!
(MS hide everything. Please switch back to old UI, this is server, not tablet!, I hate it so much when access via RDP and it's hard to open Start menu, drag/drop shortcuts to desktop, show all apps links..., finally when I wan't to logout, I can't find the menu -> have to google again!!!)

Then Tomcat & J2EE app works fine.

Then I start to install ISAPI Tomcat redirector on IIS. My old instruction for Windows 2003 is there, but the IIS 8 administration GUI is totally different.

The first thing to do is define the Tomcat ISAPI Filter. Wow, there's no ISAPI Filters menu for the website! Googling around and know that we need to install ISAPI extensions and ISAPI Filters first in the Add Roles and Features of Server Roles as below screenshot

Then back to the IIS Admin and select the server, we'll see the ISAPI and CGI Restrictions

Then open it and Add... with isapi_redirect.dll path, make sure it's 64 bit version!

Then select the website and configure ISAPI Filters for it

 Then create virtual directory named jakarta with physical path is the folder contains isapi_redirect.dll

Then allow Execute permission on it

Create jsp-examples virtual path points to any folder, like IIShome

Don't need to change the uriworkermap.properties and workers.properties.

That's it. Hope help someone to work with this issue.

Friday, July 12, 2013

Git clients and free private hosting repositories

When working with Git for puppet management and some private projects, I used eGit plugin in Geppetto and Eclipse but usually got frustrated [rejected - non-fast-forward] error when push code. Searched  around and tried some configuration but it didn't work well. Sometimes it even got wrong status between committed/pushed... So finally I switched to git command.

Until one day I found out free powerful Git and Mercurial client SourceTree that has neat GUI and easy to use.

Thought it's free we'll need to register it. Let's give it a shot, you'll like it soon.

Another issue is I used Assembla to host code free for some private projects in Git. After few months stop working with it, then backed to make a new Git project, I couldn't find the creating free Git repository link anymore. Search around and found out it stopped free plan for new projects!

So just find another one, Bitbucket is free for 5 users with issue tracker, wiki features, "clean UI", easier to use, and cool function is the import function so I could transfer source code from Assembla to Bitbucket. Very happy for this change.

Editra text editor

After over a year switching to use a MacBookPro I still missed some very useful free tools from Windows such as Notepad++, Winmerge, FreeCommander. I tried to use alternatives such as TextWrangler, and use WineBottler to build Notepad++, Winmerge, FreeCommander to run with Wine on Mac. Notepad++ and Winmerge work well except FreeCommander has problem with copy/paste.. So I find out DoubleCommander to replace FreeCommander although it neither has tree view or correct sort by folders.
But when working with Windows apps, I have to use Windows shortcuts like Ctr+C, Ctr+V instead MacOSx shortcuts and it isn't comfortable to remember to use correct shortcuts when switching between MacOSx apps & Windows apps.

Then I search around for a better text editor with projects manager or file browser. I don't like Komodo Edit because it's heavy. gedit is good with Favorites and File browser plugins but the Project Manager plugin didn't work. Fraise likes TextWrangler.

Finally I found Editra free editor with both features projects manager and file browser. Now its version is only 0.7.20 but it has a lot features - Toggle Comment, Code Folding, Bracket Highlight (doesn't work!?), Highlight Caret line, Bookmarks.., even better than Notepad++ (see more at http://editra.org/preview). The Find function is so convenient when it keep staying on the bottom of the window and continue to search when we repeat enter (I hate TextWrangler Find function when it closes the window right after the first enter - we have to use mouse click to keep it continue!). Some disadvantages are there no new .js file menu in the project context menu) although we can create a html file then rename it) or Code Browser plugin doesn't understand .js file. The text color schemes are not very good looking also, so we can spend sometime to customize it with Style Editor function.

Another good editor is Brackets Sprint editor with bunch of plugins.

But Editra is far more than I expected and I love it so much :)