19 мар. 2010 г.

Java. Загрузка данных в БД в XML формате.

В эту тяпницу зародилась у меня очередная бредовая идея. Очень часто в веб проектах возникает необходимость добавить возможность загрузки данных в БД через веб интерфейс. Проще всего сделать это путем загрузки на сервер какого-то sql файла. Или в моем случае это будет XML файл в формате DbUnit.


Итак, для начала надо понять как работает этот самый DbUnit. Есть такой интерфейс IDatabaseTester, который конфигуряется настройками соединения с БД, setUp и tearDown операциями. Ему передается объект типа IDataSet после чего можно вызывать метод onSetup() - выполнится setup операция, onTearDown() - tearDown операция.



Удобнее всего для решения поставленной задачи создать на стороне back-end-а сервис, который будет получать в качестве параметра некий InputStream и результатом работы которого будет обновленная (или нет) БД. Вот Мое решение:




// PopulationService.java
package ga.common.domain;

import java.io.InputStream;

/**
* @author Ivan Khalopik
* @version $Revision: 21 $ $Date: 2010-03-17 11:18:19 +0200 (Ср, 17 мар 2010) $
*/
public interface PopulationService {

void populate(InputStream inputStream);

}





// DbUnitPopulationService.java
package ga.common.domain;

import org.dbunit.IDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;

import java.io.InputStream;

/**
* @author Ivan Khalopik
* @version $Revision: 21 $ $Date: 2010-03-17 11:18:19 +0200 (Ср, 17 мар 2010) $
*/
public class DbUnitPopulationService implements PopulationService {
private final IDatabaseTester databaseTester;
private final FlatXmlDataSetBuilder builder;

public DbUnitPopulationService(IDatabaseTester databaseTester) {
this.databaseTester = databaseTester;
builder = new FlatXmlDataSetBuilder();
}

public void populate(InputStream inputStream) {
try {
final IDataSet dataSet = builder.build(inputStream);
databaseTester.setDataSet(dataSet);
databaseTester.onSetup();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
databaseTester.setDataSet(null);
}
}
}



Внутри сервиса на основе InputStream-а создается FlatXmlDataSet (ну или можно любой другой по желанию) и вызывается метод onSetup(). Можно пойти еще дальше и в секции catch выполнить метод onTearDown(). Однако для работы такого сервиса необходимо еще создать объект IDatabaseTester. Я предлагаю это сделать с помощью спрингового FactoryBean-а:




// DatabaseTesterFactoryBean.java
package ga.common.dbunit;

import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.IDatabaseTester;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.beans.factory.config.AbstractFactoryBean;

import javax.sql.DataSource;

/**
* @author Ivan Khalopik
* @version $Revision: 21 $ $Date: 2010-03-17 11:18:19 +0200 (Ср, 17 мар 2010) $
*/
public class DatabaseTesterFactoryBean extends AbstractFactoryBean {
private DataSource dataSource;

public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

@Override
public Class getObjectType() {
return IDatabaseTester.class;
}

@Override
protected IDatabaseTester createInstance() throws Exception {
final IDatabaseTester tester = new DataSourceDatabaseTester(dataSource);
tester.setSetUpOperation(DatabaseOperation.REFRESH);
tester.setTearDownOperation(DatabaseOperation.NONE);
return tester;
}
}



Забыл сказать, что операции бывают следующие:
• NONE - ничего не делать
• UPDATE - апдейтнуть данные
• INSERT - заинсертить данные
• REFRESH - заинсертить те, которых не хватает, апдейтнуть те, которые уже есть
• DELETE - удалить данные
• DELETE_ALL - очистить БД
• CLEAN_INSERT то же, что DELETE_ALL, а затем INSERT
Я использовал здесь REFRESH на сетапе, так как в данном случае больше всего подходит.


Итак, мы имеем сервис умеющий загружать данные в БД, теперь его можно применять на UI или в сервисах, например для автоинициализации БД при старте приложения. Например код страницы на Tapestry 5 будет выглядеть так:




// Populate.java
package ga.web.pages.security;

import ga.common.domain.PopulationService;
import ga.web.base.pages.AbstractPage;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.upload.services.UploadedFile;

/**
* @author Ivan Khalopik
* @version $Revision: 22 $ $Date: 2010-03-17 11:19:53 +0200 (Ср, 17 мар 2010) $
*/
public class Populate extends AbstractPage {

@Inject
private PopulationService populationService;

private UploadedFile file;

@Component
private Form populateForm;

public UploadedFile getFile() {
return file;
}

public void setFile(UploadedFile file) {
this.file = file;
}

public void onPopulate() {
if (populateForm.isValid()) {
populationService.populate(file.getStream());
}
}
}



Полный вариант исходников можно найти здесь


21 июл. 2009 г.

Curriculum vitae

Ivan Khalopik


Fabriciusa 11-2,
Minsk, Belarus
Mobile: +375 29 7620799
E-mail: ikhalopik@gmail.com

Summary


Experienced in software design, development and maintain about 4 years. Skilled in different java technologies. I know and apply in practice design patterns. I like to get acquainted with new technologies. I know how to work in a team. Able to solve all possible and impossible problems.

Education


Belarusian State University of Informatics and Radio electronics,
Minsk, 2007
Software for information technologies, Faculty of Computer Systems and Networks

Key skills


Process/Methodology: Continuous Integration, Test Driven Development, UML modelling.
Environment: Windows, Linux(Debian, Ubuntu, Gentoo).
Programming language: Java, HTML/CSS/JavaScript, XML, SQL, ActionScript, Perl, Python, C, C++, Delphi.
Tools: Subversion, CVS, ClearCase, Maven, IntelliJ IDEA, CruiseControl, Jira, Bugzilla, Liquibase, Apache Tomcat, JBoss.
Technology: J2EE, JSP, Servlets, JDBC, Swing, Spring, Hibernate, Toplink, Tapestry, Struts, JSF, Spring Security, JUnit, TestNG, Prototype, Dojo.
Databases: MySQL, Oracle, DB2, HSQLDB.
Languages: Russian, Belorussian, English.

Professional Experience


Software Engineer,
Tieto,
Minsk, Belarus,
August 2010 – Present


Kyriba: Software Engineer,
BelHard Development,
Minsk, Belarus,
September 2009 – July 2010

Project description: Online cash management software for businesses. Its software integrates bank balances, transaction reporting, payments, cash positioning/forecasting, automated general ledger posting, and investment portfolio reporting, among other features.
Responsibilities: Use cases analysis, design and implementation of user interface, design data model, design and implementation of business logic, communication with customer, code refactoring.
Key technologies: J2SE, J2EE, Toplink, Swing, Velocity, HTML/CSS/Javascript, Oracle, JBoss, Ant.

Students: Software Engineer,
BSUIR,
Minsk, Belarus,
January 2009 – September 2009

Project description: System of educational process. This system is used for student education processes analytics and load calculation for teachers. It has improved reporting system that is used for printing different kinds of documents.
Responsibilities: Setup of development process and environment, database design, project architectural design, create maven project structure, frameworks integration and configuration, development of infrastructure for integration tests, setting up continuous integration and build server, code refactoring, team training, code review.
Key technologies: J2SE, J2EE, Spring, Spring Security, JSF, RichFaces, Hibernate, HTML/CSS/JavaScript, Apache Tomcat, MySQL, Oracle, Hsqldb, TestNG, CruiseControl, Liquibase, Maven.

TAO: Software Engineer,
MIODAT,
Minsk, Belarus,
September 2008 – December 2008

Project description: Yahoo! advertising network extension for advanced data analytics to improve campaign optimization.
Responsibilities: Design and implementation of user interface, design and implementation of business logic, test development, code refactoring.
Key technologies: J2SE, J2EE, Spring, Spring Security, Struts 2, Hibernate, APEX, YUI, HTML/CSS/JavaScript, Liquibase, Oracle, Maven.

AdRevolver UI: Software Engineer,
MIODAT,
Minsk, Belarus,
August 2006 – September 2008

Project description: Web interface for online advertising network. Its service is intended to join behavioral targeting with advanced data analytics to improve campaign optimization over a global advertising network.
Responsibilities: Use cases analysis, design and implementation of user interface, design data model, design and implementation of business logic, code refactoring.
Key technologies: J2SE, Spring, Tapestry, Hibernate, Spring Security, HTML/CSS/JavaScript, Dojo, Apache Tomcat, MySQL, TestNG, Maven.

Additional information


Possibility for flexible working time.
Travel possibility with long travelling duration (up to 6 months).

5 июн. 2009 г.

Database population. Liquibase и DbUnit.

Очень долго не постил, хотя бредовых идей было много. Об одной сейчас расскажу.
Идея такая - захотелось поддерживать актуальность базы данных на уровне приложения, т.е. изменяем код приложения, комиттим, билдим проект и при старте обновляется БД.
До этого структура базы разрабатывалась на основе liquibase changeLog-ов и обновлялось с помощью плагина под maven. Решил, что неплохо было бы использовать этот механизм.

Как оно работает? Основной класс liquibase.Liquibase, включающий в себя основные методы для работы с БД. Необходимый мне метод - update. Для начала создадим интерфейс:

package sody.common.populator;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public interface Populator {

 void populate() throws PopulatorException;

}

package sody.common.populator;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class PopulatorException extends Exception {
 public PopulatorException() {
 }

 public PopulatorException(final String message) {
  super(message);
 }

 public PopulatorException(final String message, final Throwable cause) {
  super(message, cause);
 }

 public PopulatorException(final Throwable cause) {
  super(cause);
 }
}

Интерфейс нам понадобится в случае, если решим использовать что-то отличное от liquibase для популяции БД. Далее подключаем зависимость:

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>1.9.1</version>
</dependency>

package sody.common.populator;

import liquibase.FileOpener;
import liquibase.Liquibase;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class LiquibasePopulator implements Populator, ResourceLoaderAware {

 private ResourceLoader resourceLoader;
 private DataSource dataSource;
 private String schema;
 private String changeLogFile;
 private String contexts;
 private boolean skipPopulate;
 public static final String SCHEMA_PROPERTY = "liquibase.schema";
 private static final String CHANGE_LOG_FILE_PROPERTY = "liquibase.changeLogFile";
 public static final String SKIP_POPULATE_PROPERTY = "liquibase.skipPopulate";

 public void populate() throws PopulatorException {
  if (!skipPopulate) {
   Connection connection = null;
   try {
    connection = dataSource.getConnection();
    final Liquibase liquibase = new Liquibase(changeLogFile, new SpringResourceOpener(changeLogFile), connection);
    liquibase.getDatabase().setDefaultSchemaName(schema);
    liquibase.update(contexts);
   } catch (Exception e) {
    throw new PopulatorException("could not populate database", e);
   } finally {
    if (connection != null) {
     try {
      connection.rollback();
      connection.close();
     } catch (SQLException e) {
      //nothing to do
     }
    }
   }
  }
 }

 public void setResourceLoader(final ResourceLoader resourceLoader) {
  this.resourceLoader = resourceLoader;
 }

 public void setProperties(final Properties properties) {
  final String schema = properties.getProperty(SCHEMA_PROPERTY);
  final String changeLogFile = properties.getProperty(CHANGE_LOG_FILE_PROPERTY);
  final String skipPopulate = properties.getProperty(SKIP_POPULATE_PROPERTY);

  setSchema(schema);
  setChangeLogFile(changeLogFile);
  setSkipPopulate(Boolean.valueOf(skipPopulate));
 }

 public void setDataSource(final DataSource dataSource) {
  this.dataSource = dataSource;
 }

 public void setChangeLogFile(final String changeLogFile) {
  this.changeLogFile = changeLogFile;
 }

 public void setContexts(final String contexts) {
  this.contexts = contexts;
 }

 public void setSkipPopulate(final boolean skipPopulate) {
  this.skipPopulate = skipPopulate;
 }

 public void setSchema(final String schema) {
  this.schema = schema;
 }

 class SpringResourceOpener implements FileOpener {
  private String parentFile;

  public SpringResourceOpener(String parentFile) {
   this.parentFile = parentFile;
  }

  public InputStream getResourceAsStream(final String file) throws IOException {
   return getResource(file).getInputStream();
  }

  public Enumeration getResources(String packageName) throws IOException {
   Vector tmp = new Vector();
   tmp.add(getResource(packageName).getURL());
   return tmp.elements();
  }

  public Resource getResource(String file) {
   return resourceLoader.getResource(adjustClasspath(file));
  }

  private String adjustClasspath(String file) {
   return isClasspathPrefixPresent(parentFile) && !isClasspathPrefixPresent(file)
     ? ResourceLoader.CLASSPATH_URL_PREFIX + file
     : file;
  }

  public boolean isClasspathPrefixPresent(String file) {
   return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
  }

  public ClassLoader toClassLoader() {
   return resourceLoader.getClassLoader();
  }
 }
}

В итоге получили популятор, который при вызове метода populate читает changeLogFile и в соответствие с ним обновляет БД. Параметры можно задавать либо напрямую, либо через Properties
dataSource - DataSource нашей БД
schema - схема по умолчанию
changeLogFile - понятно
skipPopulate - отключить обновление БД

Кроме структуры, иногда нужно поддерживать актуальность содержимого БД. Это конечно можно делать с помощью того же liquibase, но это подходит только для статических, не изменяющихся данных. В редких случаях нам нужны некоторые тестовые данные, для тестового запуска приложения. Для таких случаев я решил использовать DbUnit. Все что нужно - написать еще одну реализацию нашего популятора:

package sody.common.populator;

import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.IDatabaseTester;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class DbUnitPopulator implements Populator, ResourceLoaderAware {

 private ResourceLoader resourceLoader;
 private DataSource dataSource;
 private String schema;
 private String dataSetFile;
 private boolean skipPopulate;
 private DatabaseOperation setUpOperation = DatabaseOperation.REFRESH;
 private DatabaseOperation tearDownOperation = DatabaseOperation.NONE;

 public void populate() throws PopulatorException {
  if (!skipPopulate) {
   IDatabaseTester databaseTester = new DataSourceDatabaseTester(dataSource);
   databaseTester.setSchema(schema);
   databaseTester.setSetUpOperation(setUpOperation);
   databaseTester.setTearDownOperation(tearDownOperation);
   final Resource dataSet = resourceLoader.getResource(dataSetFile);
   try {
    databaseTester.setDataSet(new FlatXmlDataSet(dataSet.getFile()));
    databaseTester.onSetup();
   } catch (Exception e) {
    throw new PopulatorException("can not populate database", e);
   }
  }
 }

 public void setProperties(final Properties properties) {
  final String schema = properties.getProperty("dbunit.schema");
  final String dataSetFile = properties.getProperty("dbunit.dataSetFile");
  final String skip = properties.getProperty("dbunit.skipPopulate");

  setSchema(schema);
  setDataSetFile(dataSetFile);
  setSkipPopulate(Boolean.valueOf(skip));
 }

 public void setSetUpOperation(final DatabaseOperation setUpOperation) {
  this.setUpOperation = setUpOperation;
 }

 public void setTearDownOperation(final DatabaseOperation tearDownOperation) {
  this.tearDownOperation = tearDownOperation;
 }

 public void setDataSource(final DataSource dataSource) {
  this.dataSource = dataSource;
 }

 public void setDataSetFile(final String dataSetFile) {
  this.dataSetFile = dataSetFile;
 }

 public void setSkipPopulate(final boolean skipPopulate) {
  this.skipPopulate = skipPopulate;
 }

 public void setSchema(final String schema) {
  this.schema = schema;
 }

 public void setResourceLoader(final ResourceLoader resourceLoader) {
  this.resourceLoader = resourceLoader;
 }
}

Получаем популятор, который при вызове метода populate читает dateSetFile и в соответствие с ним обновляет БД. Параметры как и в предыдущем примере можно задавать либо напрямую, либо через Properties
dataSource - DataSource нашей БД
schema - схема по умолчанию
dataSetFile - понятно что
skipPopulate - запретить популяцию

Осталось дело за малым - заставить популяторы запускаться со стартом приложения. Для этого создадим небольшой классик:

package sody.common.populator;

import org.springframework.beans.factory.InitializingBean;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class SpringInitializingPopulatorBean implements InitializingBean {
 private final Populator populator;

 public SpringInitializingPopulatorBean(final Populator populator) {
  this.populator = populator;
 }

 public void afterPropertiesSet() throws Exception {
  populator.populate();
 }
}

... и проделать следующее в applicationContext.xml

<bean id="liquibasePopulator" class="sody.common.populator.SpringInitializingPopulatorBean">
  <constructor-arg>
   <bean class="sody.common.populator.LiquibasePopulator"
      p:dataSource-ref="dataSource"
      p:properties-ref="jdbcProperties"
     />
  </constructor-arg>
 </bean>


 <bean id="dbUnitPopulator" class="sody.common.populator.SpringInitializingPopulatorBean"
    depends-on="liquibasePopulator">
  <constructor-arg>
   <bean class="sody.common.populator.DbUnitPopulator" depends-on="liquibasePopulator"
      p:dataSource-ref="dataSource"
      p:properties-ref="jdbcProperties"
     />
  </constructor-arg>
 </bean>

И если у нас есть бины, которые должны создаваться после популяции БД, то:

<bean id="hibernateTemplate" class="sody.common.domain.hibernate.HibernateTemplateEx" depends-on="dbUnitPopulator"
    p:sessionFactory-ref="sessionFactory"
    p:filterName="existing"/>

 <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" depends-on="dbUnitPopulator"
    p:dataSource-ref="dataSource"/>

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