среда, 26 ноября 2014 г.

Clean, safe and concise read-only Wicket Model with Java 8 Lambdas

Wicket framework uses models (IModel implementations) to bind data to components. Let's say you want to display properties of some object. Here is the data class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class User implements Serializable {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

You can do the following to display both its properties using Label components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class AbstractReadOnlyModelPanel extends Panel {
    public AbstractReadOnlyModelPanel(String id, IModel<User> model) {
        super(id, model);

        add(new Label("name", new AbstractReadOnlyModel<String>() {
            @Override
            public String getObject() {
                return model.getObject().getName();
            }
        }));
        add(new Label("age", new AbstractReadOnlyModel<Integer>() {
            @Override
            public Integer getObject() {
                return model.getObject().getAge();
            }
        }));
    }
}

Straight-forward, type-safe, but not too concise: each label requires 6 lines of code! Of course, we can reduce this count using some optimized coding conventions and so on, but anyway, anonymous classes are very verbose.

A more economical way (in terms of lines and characters to type and read) is PropertyModel.

1
2
3
4
5
6
7
8
public class PropertyModelPanel extends Panel {
    public PropertyModelPanel(String id, IModel<User> model) {
        super(id, model);

        add(new Label("name", PropertyModel.of(model, "name")));
        add(new Label("age", PropertyModel.of(model, "age")));
    }
}

It is way shorter and still pretty intuitive. But it has drawbacks:

  • First of all, it is not safe as the compiler does not check whether property named "age" exists at all!
  • And it uses reflection which does not make your web-application faster. This does not seem to be critical, but it is still a little drawback.

Luckily, Java 8 introduced lambdas and method references which allow us to create another model implementation. Here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class GetterModel<E, P> extends AbstractReadOnlyModel<P> {
    private final E entity;
    private final IModel<E> entityModel;
    private final IPropertyGetter<E, P> getter;

    private GetterModel(E entity, IModel<E> entityModel, IPropertyGetter<E, P> getter) {
        this.entity = entity;
        this.entityModel = entityModel;
        this.getter = getter;
    }

    public static <E, P> GetterModel<E, P> ofObject(E entity, IPropertyGetter<E, P> getter) {
        Objects.requireNonNull(entity, "Entity cannot be null");
        Objects.requireNonNull(getter, "Getter cannot be null");

        return new GetterModel<>(entity, null, getter);
    }

    public static <E, P> GetterModel<E, P> ofModel(IModel<E> entityModel, IPropertyGetter<E, P> getter) {
        Objects.requireNonNull(entityModel, "Entity model cannot be null");
        Objects.requireNonNull(getter, "Getter cannot be null");

        return new GetterModel<>(null, entityModel, getter);
    }

    @Override
    public P getObject() {
        return getter.getPropertyValue(getEntity());
    }

    private E getEntity() {
        return entityModel != null ? entityModel.getObject() : entity;
    }
}

... along with its support interface:

1
2
3
public interface IPropertyGetter<E, P> {
    P getPropertyValue(E entity);
}

And here is the same panel example rewritten using the new model class:

1
2
3
4
5
6
7
8
public class GetterModelPanel extends Panel {
    public GetterModelPanel(String id, IModel<User> model) {
        super(id, model);

        add(new Label("name", GetterModel.ofModel(model, User::getName)));
        add(new Label("age", GetterModel.ofModel(model, User::getAge)));
    }
}

The code is almost as concise as the version using PropertyModel, but it is:

  • type-safe: the compiler will check the actual getter type
  • defends you from typos better, because compiler will check that the getter actually exists
  • fast as it just uses regular method calls (2 per getObject() call in this case) instead of parsing property expression and using reflection

Here are the drawbacks of the described approach in comparison with PropertyModel:

  • It's read-only while PropertyModel allows to write to the property. It's easy to add ability to write using setter, but it will make code pretty clumsy, and we'll have to be careful and not use getter from one property and setter from another one.
  • PropertyModel allows to reference nested properties using the dot operator, for instance using "outerObject.itsProperty.propertyOfProperty" property expression.

But anyway, when you just need read-only models, GetterModel seems to be an interesting alternative to the PropertyModel.

And here is a little bonus: this model implementation allows to use both models and plain data objects as sources. We just need two factory methods: ofModel() and ofObject(), and we mimic the magical universality of PropertyModel (which accepts both models and POJOs as first argument) with no magic tricks at all.

понедельник, 24 ноября 2014 г.

XSLT to convert log4j.xml config to logback.xml config

Logback is a modern logging framework widely used nowadays. It has a drop-in replacement for a log4j 1.2 (the previous favorite): logback-classic. But a little problem arises if you have an application which uses log4j and want to migrate it to logback: configuration.
log4j has two configuration formats: log4j.properties and log4j.xml. For the former, everything is fine: there is log4j.properties translator script.
But for log4j.xml there doesn't seem to be any convertion tool available, and logback.xml does not understand the unconverted log4j.xml files.
So here is an XSLT which allows to convert log4j.xml files to the corresponding logback.xml configurations.
And here is an example. We have the following log4j.xml file:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <appender name="default" class="org.apache.log4j.ConsoleAppender">
        <param name="target" value="System.out"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %t %p [%c] - %m%n"/>
        </layout>
    </appender>

    <appender name="log4jremote" class="org.apache.log4j.net.SocketAppender">
        <param name="RemoteHost" value="10.0.1.10"/>
        <param name="Port" value="4712"/>
        <param name="ReconnectionDelay" value="10000"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                   value="[my-host][%d{ISO8601}]%c{1}%n%m%n"/>
        </layout>
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="LevelMin" value="ERROR"/>
            <param name="LevelMax" value="FATAL"/>
        </filter>
    </appender>

    <logger name="com.somepackage">
        <level value="INFO"/>
    </logger>

    <root>
        <level value="INFO"/>
        <appender-ref ref="default"/>
    </root>
</log4j:configuration>
Using Xalan to convert it:

java -cp xalan.jar:xercesImpl.jar:serializer.jar:xml-apis.jar org.apache.xalan.xslt.Process -IN log4j.xml -XSL log4j-to-logback.xsl -OUT logback.xml

And here is the result:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?><configuration scanPeriod="10 seconds" scan="true">
    <appender name="default" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <encoder>
            <pattern>%d %t %p [%c] - %m%n</pattern>
        </encoder>
    </appender>

<appender name="log4jremote" class="ch.qos.logback.classic.net.SocketAppender">
        <remoteHost>10.0.1.10</remoteHost>
        <port>4712</port>
        <reconnectionDelay>10000</reconnectionDelay>
        <!-- this is NOT needed tor this logger, so it is commented out -->
        <!--
        <layout>
            <pattern>[my-host][%d{ISO8601}]%c{1}%n%m%n</pattern>
        </layout>
                    -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
    </appender>

<logger name="com.somepackage" level="INFO"/>

<root level="INFO">
        <appender-ref ref="default"/>
    </root>
</configuration>

The script translates the basic loggers (console/email/file/syslog/socket).

Here is github repository: https://github.com/rpuch/log4j2logback

пятница, 3 октября 2014 г.

Spring Security 3.2+ defaults break Wicket Ajax-based file uploads

A couple of days ago we have run into a bug: we found that file uploads in our Wicket application has broken. Instead of working as expected, upload button did not work, instead a message appeared in the browser console (this one is for Chrome):
Refused to display 'http://localhost:8084/paynet-ui/L7ExSNbPC4sb6TPJDblCAkN0baRJxw3q6-_dANoYsTD…QK61FV9bCONpyleIKW61suSWRondDQjTs8tjqJJOpCEaXXCL_A%2FL7E59%2FTs858%2F9QS3a' in a frame because it set 'X-Frame-Options' to 'DENY'.
That seemed strange, because X-Frame-Options relates to frames which we didn't use explicitly. But when file upload is made using Ajax, Wicket carries this out using an implicit Frame.
Spring Security started adding this header starting with version 3.2, so it was actually an upgrade to Spring Security 3.2 that broke file uploads. To sort this out, it was sufficiently to change the X-Frame-Options value from DENY to SAMEORIGIN using the following snippet in web security configuration (created using @Configuration-based approach):
http
    .headers()
        .contentTypeOptions()
        .xssProtection()
        .cacheControl()
        .httpStrictTransportSecurity()
        .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
File uploads work now, the quest is finished.