10 апр. 2009 г.

Hibernate. Интернационализация БД. О том как плохо получилось.

Снова пятница (не тяпница, по тяпницам у меня хорошее настроение), на улице дождь, настроение в ноль. Целую неделю прихожу на работу на час раньше, ну не дурень разве?)
В добавок ко всему больной... Ну да ладно о плохом, раскажу ка я лучше о своей очередной бредовой идее.
Проснулся как то ночью и решил, хочу чтобы мои справочники в БД были локализованы... Начал рыться по нету, что да как... Суть в том, что есть какие то сущности замапленые с помощью hibernate на реляционную БД, у этих сущностей есть нейкий description, который и нужно локализировать.

Так вот, подумал немного и кое как с горем попалам придумал кое чего, хотя мне кажется, что уж слишком грязно все получилось. Ну да ладно, как бы не было стыдно, покажу то, что у меня получилось.
Для начала сделал вот такой интерфейсик, для интернационализации чего-то, и 2 его реализации
package itdep.common.i18n;

import java.util.Locale;

/**
* @author Ivan Khalopik
* @version $Revision: 267 $ $Date: 2009-04-09 09:38:38 +0300 (Чт, 09 апр 2009) $
*/
public interface I18n<T> {

  T getValue(final Locale locale);

  void setValue(final Locale locale, final T value);

}

package itdep.common.i18n.impl;

import itdep.common.i18n.I18n;

import java.util.Locale;

/**
 * @author Ivan Khalopik
 * @version $Revision: 267 $ $Date: 2009-04-09 09:38:38 +0300 (Чт, 09 апр 2009) $
 */
public class SimpleI18n<T> implements I18n<T> {
  private T value;

  public SimpleI18n() {
  }

  public SimpleI18n(final T value) {
    this.value = value;
  }

  public T getValue(final Locale locale) {
    return value;
  }

  public void setValue(final Locale locale, final T value) {
    this.value = value;
  }

  @Override
  public String toString() {
    return String.valueOf(value);
  }
}

package itdep.common.i18n.impl;

import itdep.common.i18n.I18n;
import itdep.common.util.I18nUtils;

import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * @author Ivan Khalopik
 * @version $Revision: 268 $ $Date: 2009-04-09 11:55:04 +0300 (Чт, 09 апр 2009) $
 */
public class I18nMapWrapper<T> implements I18n<T> {
  private final Map<Locale, T> values;

  public I18nMapWrapper(final Map<Locale, T> values) {
    this.values = values;
  }

  public T getValue(final Locale locale) {
    final List<Locale> candidates = I18nUtils.getCandidateLocales(locale);
    for (Locale candidate : candidates) {
      if (values.containsKey(candidate)) {
        return values.get(candidate);
      }
    }
    return null;
  }

  public void setValue(final Locale locale, final T value) {
    values.put(locale, value);
  }

  @Override
  public String toString() {
    return String.valueOf(getValue(Locale.getDefault()));
  }
}

Первая реализация - это что вроде заглушки, когда объект во всех локалях имеет одно и то же значение, во вторую подсовываем мапу с ключем в виде локали, из которой в соответсвии с нужной локалью и будет браться локализированое значение.
Далее все это нужно совместить с hibernate сущностью. Для этого объявил интерфейс:<>
package itdep.common.i18n;

import itdep.common.dao.Entity;

import java.io.Serializable;

/**
 * @author Ivan Khalopik
 * @version $Revision: 265 $ $Date: 2009-04-08 11:37:12 +0300 (Ср, 08 апр 2009) $
 */
public interface I15dEntity<PK extends Serializable> extends Entity<PK> {

  I18n<String> getDescription();

}

Ну и реализация
package itdep.model;

import itdep.common.i18n.I18n;
import itdep.common.i18n.impl.I18nMapWrapper;
import org.hibernate.annotations.CollectionOfElements;
import org.hibernate.annotations.MapKey;

import javax.persistence.*;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * @author Ivan Khalopik
 * @version $Revision: 265 $ $Date: 2009-04-08 11:37:12 +0300 (Ср, 08 апр 2009) $
 */
@MappedSuperclass
public abstract class CodedEntity extends SomeEntity implements I15dEntity<Long> {

  @Column(name = "CODE", nullable = true, insertable = true, updatable = true)
  private String code;

  @CollectionOfElements(fetch = FetchType.EAGER)
  @JoinTable(name = "I18N",
    joinColumns = @JoinColumn(name = "CODE", referencedColumnName = "CODE", nullable = false))
  @MapKey(columns = @Column(name = "LOCALE", nullable = true, insertable = true, updatable = true))
  @Column(name = "VALUE", nullable = false, insertable = true, updatable = true)
  private Map<Locale, String> i15dName = new HashMap<Locale, String>();

  public String getCode() {
    return code;
  }

  public void setCode(final String code) {
    this.code = code;
  }

  @Override
  public I18n<String> getDescription() {
    return new I18nMapWrapper<String>(i15dName);
  }
}

Здесь для локализации используется таблица I18N с полями CODE(код), LOCALE(локаль), VALUE(значение), в самой таблице сущности храним поле CODE, которое и соотетсвует тому, что в I18N. В итоге получаем, что #getDescription() возвращает нам локализированую строку. Ну а в случае, если надо, чтоб возвращалось одно и то же значение во всех локалях можно сделать так:
package itdep.model;

import itdep.common.i18n.I18n;
import itdep.common.i18n.impl.SimpleI18n;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

/**
 * @author Ivan Khalopik
 * @version $Revision: 265 $ $Date: 2009-04-08 11:37:12 +0300 (Ср, 08 апр 2009) $
 */
@MappedSuperclass
public abstract class NamedEntity extends SomeEntity implements I15dEntity<Long> {

  @SuppressWarnings({"UnusedDeclaration"})
  @Column(name = "NAME", nullable = false, insertable = true, updatable = true)
  private String name;

  @Override
  public I18n<String> getDescription() {
    return new SimpleI18n<String>(name);
  }
}

Ну вот что-то и получилось, но мне не нравиться, буду видимо переделывать, хочу реализовать отображение через спринговый MessageSource, а редактирование можно и так, немного переделать, или, возможно, что-то совсем по другому сделать. Вобщем продолжение следует.
Все, пойду лечиться...

3 апр. 2009 г.

TestNG и о том как привести результаты к совместимым с JUnit

Опять тяпница, опять хорошее предпразничное настроение и опять мой мозг разрывается в выборе из той кучи бестолковых идей, которые в нем витают...

Однажды переводил тесты из JUnit на TestNG и столкнулся с одной маленькой проблеммкой. А проблеммка эта заключается в том, что CruiseControl нивкакую не соглашается понимать результаты тестов от TestNG, даже за пиво :)


Покопался в инете, нашел одну вот такую вещь:
TestNG contains a listener that takes the TestNG results and outputs an XML file that can then be fed to JUnitReport.

В общем все решается просто - одним антовским таксом:

<target name="reports">
<junitreport todir="test-report">
<fileset dir="test-output">
<include name="**/*.xml"/>
</fileset>
<report format="noframes" todir="test-report"/>
</junitreport>
</target>

Но здесь есть еще одно но. Мой проект не использует ant, все базируется на maven, и мне бы очень не хотелось добавлять в него еще и антовский таск. На помощь мне в сложившейся ситтуации пришел плагин maven-antrun-plugin. Все что нужно, это переместить антовский таск в мавеноский pom.xml и делается это следующим образом:


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<configuration>
<tasks>
<junitreport todir="${project.build.directory}/surefire-reports">
<fileset dir="${project.build.directory}/surefire-reports">
<include name="**/*.xml"/>
</fileset>
<report format="noframes" todir="${project.build.directory}/surefire-reports"/>
</junitreport>
</tasks>
</configuration>
</plugin>

Теперь запускаем antrun:run и... все готово. Тесты сконвертились, CruiseControl доволен, а мы идем пить тяпничное пиво.

27 мар. 2009 г.

Cruise Control. О том, да о сем...

Наступила очередная пятница, день можно сказать почти выходной, и я решил создать свой блог, в котором намереваюсь рассказывать о воплощении своих бредовых идей...

Не долго думая решил рассказать об одной такой. Зародилась как то у меня идея прикрутить к проекту, над которым в данный момент работаю, continuous integration tool под названием Cruise Control. Прочитал все за и против, решил, это как раз то, что мне надо. Осталось дело за малым - установка и настройка, ох и поворочался я с этим...


Итак, что мы имеем? Сервер на базе debian lenny с установленным tomcat 5.5, проект, лежащий в репозитории под управлением subversion и собирающийся при помощи maven2.
Для начала скачаем последнюю версию с офф. сайта
Распаковываем в /opt/cruisecontrol.
Затем я пошел дальше и решил немного изменить структуру всего этого счастья:
/opt/cruisecontrol/lib - наши либы, как было, так и оставил
/opt/cruisecontrol/webapps - веб проекты, не трогал
/opt/cruisecontrol/bin - сюда перенес скрипт запуска cruisecontrol.sh
/etc/cruisecontrol - настройки, cruisecontrol.xml dashboard.xml log4j.xml
/var/cache/cruisecontrol - папка используемая нашим веб приложением
/var/log/cruisecontrol - логи
/var/spool/cruisecontrol - все, что относится к нашим проектам
/var/spool/cruisecontrol/projects - рабочие папки проектов
/var/spool/cruisecontrol/artifacts - собранные артефакты
/var/spool/cruisecontrol/logs - логи сборки

Заменил содержимое cruisecontrol.sh

#!/bin/sh

basename=`basename $0`

function usage() {
exit 1
}

# The root of the CruiseControl directory. The key requirement is that this is the parent
# directory of CruiseControl's lib and dist directories.
if [ -z "$CCDIR" ]; then
CCDIR=/opt/cruisecontrol
fi

LIBDIR=$CCDIR/lib

temp=`getopt -o "" -a --long "port:,webport:,rmiport:,configfile:,log:,classpath:,basedir:,dashboardurl:,daemon,jetty" -n $basename -- "$@"`
if [ $? != 0 ]; then
usage
fi

eval set -- "$temp"

while [ "$1" != "--" ]; do
case "$1" in
--configfile)
configfile=$2
shift 2
;;
--jmxport|--port)
port=$2
shift 2
;;
--classpath)
CRUISE_PATH="$CRUISE_PATH:$2"
shift 2
;;
--log)
log=$2
shift 2
;;
--basedir)
basedir=$2
shift 2
;;
--dashboardurl)
dashboardurl=$2
shift 2
;;
--daemon)
daemon=true
shift 1
;;
--jetty)
jetty=true
shift 1
;;
--webport)
webport=$2
shift 2
;;
--rmiport)
rmiport=$2
shift 2
;;
*)
usage
;;
esac
done

# create the CruiseControl arguments (configfile, port)
[ -z "$configfile" ] && usage
CRUISE_OPTS="-configfile $configfile"
[ -n "$log" ] && CRUISE_OPTS="$CRUISE_OPTS -log4jconfig $log"
[ -n "$port" ] && CRUISE_OPTS="$CRUISE_OPTS -jmxport $port"
[ -n "$rmiport" ] && CRUISE_OPTS="$CRUISE_OPTS -rmiport $rmiport"
[ -n "$dashboardurl" ] && CRUISE_OPTS="$CRUISE_OPTS -dashboardurl $dashboardurl"

JAVA_OPTIONS="$JAVA_OPTIONS -Djavax.management.builder.initial=mx4j.server.MX4JMBeanServerBuilder -Dcc.library.dir=$LIBDIR"

# choose the JVM executable
JAVA_CMD="java"
[ -n "$JAVA_HOME" ] && JAVA_CMD="$JAVA_HOME/bin/java"
LAUNCHER="$LIBDIR/cruisecontrol-launcher.jar"

# if necessary, change to the base directory
[ -n "$basedir" ] && cd $basedir

if [ -n "$jetty" ]; then
JAVA_OPTIONS="$JAVA_OPTIONS -Djetty.logs=/var/log/cruisecontrol"

# CRUISE_MAIN=CruiseControlWithJetty
[ -n $webport ] && CRUISE_OPTS="$CRUISE_OPTS -webport $webport"

fi

EXEC="$JAVA_CMD $JAVA_OPTIONS -jar $LAUNCHER $@ $CRUISE_OPTS"

if [ -n "$daemon" ]; then
default_outputfile="/var/log/cruisecontrol/cruisecontrol.out"
if [ -z "$CRUISE_OUTPUTFILE" ]; then
CRUISE_OUTPUTFILE=$default_outputfile
fi

if [ ! -w "$CRUISE_OUTPUTFILE" ]; then
echo "$CRUISE_OUTPUTFILE isn't writable, output redirected to /dev/null"
CRUISE_OUTPUTFILE="/dev/null"
fi

echo $EXEC >> "$CRUISE_OUTPUTFILE"

$EXEC 2>&1 >> "$CRUISE_OUTPUTFILE" &

if [ -z "$CRUISE_PIDFILE" ]; then
CRUISE_PIDFILE="/var/run/cruisecontrol.pid"
fi

if [ -w "$CRUISE_PIDFILE" ]; then
echo $! > "$CRUISE_PIDFILE"
else
echo "$CRUISE_PIDFILE isn't writable, the JVM pid is $!"
fi
else
exec $EXEC
fi
]]>

Создал пользователя и привилегии на папки:

adduser --system --home /var/spool/cruisecontrol --no-create-home --ingroup nogroup --disabled-password --shell /bin/false ccontrol
chown ccontrol:adm -hR /var/log/cruisecontrol /var/spool/cruisecontrol
chown tomcat55:adm -hR /var/cache/cruisecontrol
chmod -R 755 /var/log/cruisecontrol /var/spool/cruisecontrol
chmod -R 775 /var/cache/cruisecontrol
ln -s /opt/cruisecontrol/bin/cruisecontrol /usr/bin/cruisecontrol

Для автоматического запуска нашего гиганта добавил файлы /etc/default/cruisecontrol и /etc/init.d/cruisecontrol следующего содержания:

#
# /etc/default/cruisecontrol
#
# Run CruiseControl as this user ID (default: ccontrol)
#
# Set this to an empty string to prevent CruiseControl from starting
# automatically. Think to stop CruiseControl before setting an empty string ;o)
#
# CRUISE_USER="root"

# Port used for the JMX Http Console
#CRUISE_PORT=8000

# To activate Jetty support
#CRUISE_JETTY=true

# Port used by the embedded Jetty server (if CRUISE_JETTY is true)
#CRUISE_JETTYPORT=8080

# CRUISE_RMIPORT
# Port used by the MBeanServer RMIConnector
# defaults: disabled
#CRUISE_RMIPORT=1099

CRUISE_DASHBOARD="http://localhost:8180/dashboard"
# The home directory of the Java development kit (JDK).
#JAVA_HOME="/usr/local/j2sdk1.3.1"
#JAVA_HOME="/usr/local/j2sdk1.4.0"

# JVM options
#JAVA_OPTIONS="-Xmx512m"

# Arguments to pass to the CruiseControl script
#CRUISE_OPTS=""

#
# Clean options
# used by /etc/cron.daily/cruisecontrol
#

# CRUISE_CLEANCACHE
# Max days to keep the cache (/var/cache/cruisecontrol) files
# defaults: 7

#CRUISE_CLEANCACHE=7

# CRUISE_CLEANLOGS
# Max days to keep the logs (/var/spool/cruisecontrol/logs) files
# defaults: none

#CRUISE_CLEANLOGS=7



#! /bin/sh -e
#
# /etc/init.d/cruisecontrol -- startup script for the CruiseControl engine
#
# Writed by Alban Peignier .
# Based on the tomcat4 init script

PATH=/usr/bin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
NAME=cruisecontrol
DESC="CruiseControl loop engine"
DAEMON=/usr/bin/$NAME

# The following variables can be overwritten in /etc/default/cruisecontrol

# Run CruiseControl as this user ID (default: ccontrol)
# Set this to an empty string to prevent CruiseControl from starting automatically
CRUISE_USER=ccontrol
CRUISE_PORT=8000
CRUISE_JETTYPORT=8080

# Arguments to pass to the Java virtual machine (JVM)
CRUISE_OPTS=""

# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not
# defined in /etc/default/cruisecontrol)
JDK_DIRS=`ls -d /usr/lib/jvm/* /usr/lib/j2sdk* /usr/lib/j2re* 2> /dev/null || true`

# End of variables that can be overwritten in /etc/default/cruisecontrol

# overwrite settings from default file
if [ -f /etc/default/cruisecontrol ]; then
. /etc/default/cruisecontrol
fi

test -f $DAEMON || exit 0

function check_for_no_start() {
if [ -z "$CRUISE_USER" ]; then
echo "Not starting/stopping $DESC as configured (CRUISE_USER is"
echo "empty in /etc/default/cruisecontrol)."
exit 0
fi
}

# Look for the right JVM to use
for jdir in $JDK_DIRS; do
if [ -d "$jdir" -a -z "${JAVA_HOME}" ]; then
JAVA_HOME="$jdir"
fi
done
export JAVA_HOME

if [ -z "$JAVA_HOME" ]; then
echo "Could not start $DESC because no Java Development Kit"
echo "(JDK) was found. Please download and install JDK 1.3 or higher and set"
echo "JAVA_HOME in /etc/default/cruisecontrol to the JDK's installation directory."
exit 0
fi

[ -n "$JAVA_OPTIONS" ] && export JAVA_OPTIONS

CRUISE_OPTS="$CRUISE_OPTS -configfile /etc/cruisecontrol/cruisecontrol.xml"
CRUISE_OPTS="$CRUISE_OPTS -port $CRUISE_PORT"
CRUISE_OPTS="$CRUISE_OPTS -log file:/etc/cruisecontrol/log4j.xml"
CRUISE_OPTS="$CRUISE_OPTS -basedir /var/spool/cruisecontrol"
CRUISE_OPTS="$CRUISE_OPTS -daemon"

if [ "$CRUISE_JETTY" = "true" ]; then
CRUISE_OPTS="$CRUISE_OPTS -jetty -webport $CRUISE_JETTYPORT"
fi

if [ -n "$CRUISE_RMIPORT" ]; then
CRUISE_OPTS="$CRUISE_OPTS -rmiport $CRUISE_RMIPORT"
fi

if [ -n "$CRUISE_DASHBOARD" ]; then
CRUISE_OPTS="$CRUISE_OPTS -dashboardurl $CRUISE_DASHBOARD"
fi

export CRUISE_OPTS

# Define other required variables
LOGDIR="/var/log/cruisecontrol"

CRUISE_PIDFILE="/var/run/$NAME.pid"
CRUISE_OUTPUTFILE=$LOGDIR/cruisecontrol.out

export CRUISE_PIDFILE CRUISE_OUTPUTFILE

case "$1" in
start)
check_for_no_start
echo -n "Starting $DESC: "

touch "$CRUISE_PIDFILE" "$LOGDIR/cruisecontrol.out" || true
chown --dereference "$CRUISE_USER" "$CRUISE_PIDFILE" "$LOGDIR" \
"$LOGDIR/cruisecontrol.out" || true
if start-stop-daemon --test --start --pidfile "$CRUISE_PIDFILE" \
--user $CRUISE_USER --startas "$JAVA_HOME/bin/java" \
>/dev/null; then
# -p preserves the environment (for $JAVA_HOME etc.)
# -s is required because cruisecontrol's login shell is /bin/false
su -p -s /bin/sh $CRUISE_USER \
-c "\"$DAEMON\" $CRUISE_OPTS" \
>>"$LOGDIR/cruisecontrol.out" 2>&1
echo "$NAME."
else
echo "(already running)."
fi
;;
stop)
echo -n "Stopping $DESC: "
if start-stop-daemon --test --start --pidfile "$CRUISE_PIDFILE" \
--startas "$JAVA_HOME/bin/java" \
>/dev/null; then
echo "(not running)."
else
stop_options=""
if [ -n "$CRUISE_USER" ]; then
stop_options="--user $CRUISE_USER"
fi

start-stop-daemon --stop --oknodo --quiet \
--pidfile "$CRUISE_PIDFILE" $stop_options
rm -f "$CRUISE_PIDFILE"
echo "$NAME."
fi
;;
restart|force-reload)
check_for_no_start

$0 stop
sleep 1
$0 start
;;
*)
#echo "Usage: /etc/init.d/cruisecontrol {start|stop|restart|reload|force-reload}" >&2
echo "Usage: /etc/init.d/cruisecontrol {start|stop|restart|force-reload}" >&2
exit 1
;;
esac

exit 0

Далее

update-rc.d cruisecontrol defaults
invoke-rc.d cruisecontrol start

Теперь запуск Cruise Control будет автоматическим. Но чего то не хватает... Веб приложение! Для того, чтобы все заработало как надо, необходимо немного подправить некоторые настройки. Итак, /opt/cruisecontrol/webapps/cruisecontrol/WEB-INF/web.xml, находим и правим параметры:
- cacheRoot на /var/cache/cruisecontrol
- logDir на /var/spool/cruisecontrol/logs
- rootDir на /var/spool/cruisecontrol/artifacts
Теперь настроим dashboard. Лезем в /etc/default/tomcat5.5 правим

JAVA_OPTS="-Ddashboard.config=/etc/cruisecontrol/dashboard.xml"

/etc/cruisecontrol/dashboard.xml

<buildloop logsdir="/var/spool/cruisecontrol/logs" artifactsdir="/var/spool/cruisecontrol/artifacts"/>

CC готов к работе! Осталось настроить проекты, но об этом в другой раз...