17 апр. 2009 г.

Blogger. Подсветка синтаксиса - не совсем бесполезная идея.

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

Первый - с помощью javascript-а, мне понравился проект от Google под скромным названием google-code-prettify. Для активации необходимо добавить в хеадеры строки
<link href='http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.css' rel='stylesheet' type='text/css'/>
    <script src='http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.js' type='text/javascript'/> 
Далее необходимо в тег body добавить onload='prettyPrint()'.На примере blogger.com для этого нужно подправить шаблон страницы Layout->Edit HTML. В итоге получаем все, что заключено в теги code или pre с выставленным аттрибутом class='prettyprint' будет отформатировано в соответствие с синтаксисом какого либо языка. По умолчанию, язык определяется автоматически, для того, чтобы задать конкретный язык необходимо выставить атрибут class='prettyprint-lang', где вместо lang язык из списка (bsh, c, cc, cpp, cs, csh, cyc, cv, htm, html, java, js, m, mxml, perl, pl, pm, py, rb, sh, xhtml, xml, xsl)

Второй способ - с помощью сайтов постеров кода, как пример - quick highlighter. Все что нужно - запостить свой код и получить готовый отформарматированный html-код. На выше упомянутом сайте есть два способа генерации html-кода - отдельно css и html (css нужно преварительно поместить в хеадеры) и все вместе(все в html-коде). На примере blogger.com для добавления css в хеадеры нужно опять же подправить шаблон страницы, для этого Layout->Edit HTML.

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 готов к работе! Осталось настроить проекты, но об этом в другой раз...