23 February, 2012

Упрощаем работу с JPA при помощи Spring Data JPA

Введение

Уже прошло несколько лет с тех пор, как появился JPA. Работа с EntityManager увлекательна, но разработчики пишут красивый API, а подробности работы с базой данных скрывают. При этом частая проблема - дублирование имплементации, когда из одного DAO в другой у нас плавно перекочёвывает один и тот же код, в лучшем случае этот код переносится в абстрактный базовый DAO. Spring Data коренным образом решает проблему - при его использовании остаётся только API на уровне интерфейсов, вся имплементация создаётся автоматически с использованием AOP.

История Spring Data

Несмотря на то, что проект только недавно достиг версии 1.0, у него достаточно богатая история - раньше он развивался в рамках проекта Hades.

Объявление DAO-интерфейса

Итак, для начала нам необходимо объявить DAO-интерфейс, в котором мы будем объявлять методы для работы с сущностью.
public interface UserRepository extends CrudRepository<User, Long> {
}
Данного кода достаточно для обычного DAO с CRUD-методами.
  • save - сохраняет или обновляет переданную сущность.
  • findOne - ищет сущность по первичному ключу.
  • findAll - возвращает коллекцию всех сущностей
  • count - возвращает количество сущностей
  • delete - удаляет сущность
  • exists - проверяет, существует ли сущность с данным первичным ключом
Полный список методов, объявленный в CrudRepository можно посмотреть в javadoc. В случае, если нам нужны не все методы, то есть возможность произвести наследование от интерфейса Repository и перенести в наследника только те методы из интерфейса CrudRepository, которые нужны.

Поддержка сортировки и постраничного просмотра

Очень часто требующаяся функциональность - это возможность возвращать только часть сущностей из БД, например, для реализации постраничного просмотра в пользовательском интерфейсе. Spring Data и тут хорош и предоставляет нам возможность добавить такую функциональность в наш DAO. Для этого достаточно добавить объявление следующего метода в наш DAO-интерфейс:
 Page<User> findAll(Pageable pageable);
Интерфейс Pageable инкапсулирует в себе сведения о номере запрашиваемой страницы, размере страницы, а также требуемой сортировке.

Ищем данные

Как правило, на обычных CRUD-ах DAO не заканчиваются и часто требуются дополнительные методы, которые возвращают только те сущности, которые удовлетворяют заданным условиям. На мой взгляд, Spring Data сильно упрощает жизнь в данной области. Например, нам нужен методы для поиска пользователя по логину и по его e-mail адресу:
 User findByLogin(String login);
 User findByEmail(String email);
Все просто. В случае, если нужны более сложные условия для поиска, то и это тоже реализовано. Spring Data поддерживает следующие операторы:
  • Between
  • IsNotNull
  • NotNull
  • IsNull
  • Null
  • LessThan
  • LessThanEqual
  • GreaterThan
  • GreaterThanEqual
  • NotLike
  • Like
  • NotIn
  • In
  • Near
  • Within
  • Regex
  • Exists
  • IsTrue
  • True
  • IsFalse
  • False
  • Not
Такой внушительный список открывает простор для фантазии, так что можно составить сколь угодно сложный запрос. Если необходимо, чтобы в результатах поиска было несколько сущностей, то необходимо называть метод findAllByBlahBlah

Поддержка Spring MVC

Это часть основана на официальной документации. Представьте, что вы разрабатываете веб-приложение с использованием Spring MVC. Тогда вам необходимо будет загружать сущность из базы данных используя параметры HTTP-запроса. Это может выглядеть следующим образом:
@Controller
@RequestMapping("/users")
public class UserController {

  private final UserRepository userRepository;

  public UserController(UserRepository userRepository) {
    userRepository = userRepository;
  }

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") Long id, Model model) {
    
    // Do null check for id
    User user = userRepository.findOne(id);
    // Do null check for user
    // Populate model
    return "user";
  }
}
Во-первых, вы объявляете зависимость на DAO, а во-вторых всегда вызываете метод findOne() для загрузки сущности. К счастью, Spring позволяет нам преобразовывать строковые значения из HTTP-запросов в любой нужный тип используя либо PropertyEditor, либо ConversionService. Если вы используете Spring версии 3.0 и выше, то вам необходимо добавить следующую конфигурацию:
<mvc:annotation-driven conversion-service="conversionService" />
<bean id="conversionService" class="….context.support.ConversionServiceFactoryBean">
  <property name="converters">
    <list>
      <bean class="org.springframework.data.repository.support.DomainClassConverter">
        <constructor-arg ref="conversionService" />
      </bean>
    </list>
  </property>
</bean>
Если же вы используете Spring более старой версии, то вам необходима вот такая конфигурация:
<bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="webBindingInitializer">
    <bean class="….web.bind.support.ConfigurableWebBindingInitializer">
      <property name="propertyEditorRegistrars">
        <bean class="org.springframework.data.repository.support.DomainClassPropertyEditorRegistrar" />
      </property>
    </bean>
  </property>
</bean>
После данных изменений в конфигурации можно переписать контроллер следующим образом:

@Controller
@RequestMapping("/users")
public class UserController {

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") User user, Model model) {

    // Do null check for user
    // Populate model
    return "userForm";
  }
}
Обратите внимание на то, как упростился код и как мы красиво избавились от его дублирования.

Документация

На данный момент документации по проекту не так уж и много, но, тем не менее, она есть:

Заключение

Spring Data значительно упрощает жизнь при использовании JPA. Рекомендуется к использованию в своих проектах.

09 February, 2012

Разработка и тестирование Java REST веб-сервисов

Введение

Для разработки REST веб-сервисов Java предлагает JSR-311 - JAX-RS: The Java™ API for RESTful Web Services Как это обычно бывает в мире Enterprise Java, существует несколько реализаций данной спецификации: На примере последней реализации, я и расскажу, каким образом можно написать REST-сервис.

Пишем REST-сервис


import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;

@Path("/service/entity")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public interface EntityRestService {

 @GET
 @Path("/all")
 EntityList listAll();

 @GET
 @Path("/{id}")
 Entity findById(@PathParam("id") Integer id);


}
Создаём интерфейс, в котором расставляем JAX-RS аннотации. Аннотация @Path указывает путь, по которому будет доступен наш сервис. Аннотация @GET определяет, какой HTTP-запрос будет обрабатываться данным методом. Аннотация @Produces позволяет указать, в каком формате данный сервис предоставляет результаты.

Конфигурация JBoss RESTEasy

Конфигурация очень проста, во-первых, нам нужно добавить обновить pom.xml и добавить необходимые зависимости:
Обновляем pom.xml
Добавляем compile-time зависимость на JAX-RS API
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>jaxrs-api</artifactId>
</dependency>
и runtime зависимости на реализацию
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <version>${resteasy-jaxrs.version}</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson-provider</artifactId>
    <version>${resteasy-jaxrs.version}</version>
    <scope>runtime</scope>
</dependency>

Обновляем конфигурацию веб-приложения в web.xml
Необходимо выполнить следующую модификацию web.xml
 <listener>
  <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
 </listener>

 <context-param>
  <param-name>resteasy.servlet.mapping.prefix</param-name>
  <param-value>/rest</param-value>
 </context-param>
 <servlet>
  <servlet-name>REST Easy</servlet-name>
  <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
 </servlet>
 <servlet-mapping>
  <servlet-name>REST Easy</servlet-name>
  <url-pattern>/rest/*</url-pattern>
 </servlet-mapping>
Данный код объявляет сервлет HttpServletDispatcher, который будет обрабатывать все запросы, которые приходят на /rest/*. Слушатель ResteasyBootstrap выполняет всю необходимую инициализацию JBoss RESTEasy.
Поддержка Spring
Для того, чтобы использовать Spring Framework, нам необходимо сделать следующие изменения: 1. Добавить зависимость в pom.xml
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-spring</artifactId>
    <version>${resteasy-jaxrs.version}</version>
    <scope>runtime</scope>
</dependency>
2. Добавить Spring-ового слушателя в web.xml
 <listener>
  <listener-class>org.jboss.resteasy.plugins.spring.SpringContextLoaderListener</listener-class>
 </listener>

После этих изменений JBoss RESTEasy будет в курсе про Spring, это позволит в реализации REST-сервисов полноценно использовать все возможности, предоставляемые Spring Framework.

Пишем тесты

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/applicationContext-test.xml"})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class})
public class TestEntityServiceRest {

 private static final int PORT = 8081;
 private static final String BASE_URL = "http://localhost:" + PORT;

 @Autowired
 EntityService entityService;

 @Autowired
 ConfigurableApplicationContext applicationContext;


 protected HttpClient client;
 protected TJWSEmbeddedJaxrsServer server;


 @Before
 public void setUpClient() throws Exception {
  client = new DefaultHttpClient();
 }

 @Before
 public void setUpServer() throws Exception {
  server = new TJWSEmbeddedJaxrsServer();
  server.setPort(PORT);
  ResteasyDeployment deployment = server.getDeployment();

  server.start();

  Dispatcher dispatcher = deployment.getDispatcher();
  SpringBeanProcessor processor = new SpringBeanProcessor(dispatcher, deployment.getRegistry(), deployment.getProviderFactory());
  applicationContext.addBeanFactoryPostProcessor(processor);

  SpringResourceFactory noDefaults = new SpringResourceFactory(
    "entityServiceRestImpl", applicationContext, EntityRestServiceImpl.class);
  dispatcher.getRegistry().addResourceFactory(noDefaults);
 }

 @After
 public void stop() {
  server.stop();
 }

 @Test
 public void testListAll() throws Exception {

  int i = 0;
  final List<Entity> returnedList = new ArrayList<Entity>();
  int EXPECTED_SIZE = 6;
  while (i < EXPECTED_SIZE) {
   i++;
   final Entity entity = new Entity();
   entity.setId(i);
   entity.setName("test" + i);
   returnedList.add(entity);
  }
  when(entityService.findAll()).thenReturn(returnedList);


  HttpGet get = new HttpGet(BASE_URL + "/service/entity/");

  HttpResponse response = client.execute(get);
  InputStream content = response.getEntity().getContent();

  EntityList result = fromString(EntityList.class, content);
  content.close();

  Assert.assertNotNull(result);
  Assert.assertEquals(EXPECTED_SIZE, result.getEntities().size());
 }

 @Test
 public void testFindById() throws Exception {

  final Entity validEntity = getValidEntity();
  validEntity.setId(2);

  when(entityService.findById(validEntity.getId())).thenAnswer(new Answer<Object>() {
   @Override
   public Object answer(InvocationOnMock invocation) throws Throwable {
    return validEntity;
   }
  });

  HttpGet get = new HttpGet(BASE_URL + "/service/entity/2");

  HttpResponse response = client.execute(get);
  InputStream content = response.getEntity().getContent();

  Entity result = fromString(Entity.class, content);
  content.close();

  Assert.assertNotNull(result);

  deepAssertEquals(validEntity, result);

 }

 private void deepAssertEquals(Object expected, Object actual) throws IllegalAccessException, InvocationTargetException {
  Assert.assertEquals(expected.getClass(), actual.getClass());
  PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(expected.getClass());
  for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
   Object expectedValue = propertyDescriptor.getReadMethod().invoke(expected);
   Object actualValue = propertyDescriptor.getReadMethod().invoke(actual);
   Assert.assertEquals(expectedValue, actualValue);
  }
 }

        public static <T> T fromString(Class<T> clazz, String input) throws JAXBException {
  JAXBContext ctx = JAXBContext.newInstance(clazz);
  Unmarshaller unmarshaller = ctx.createUnmarshaller();
  @SuppressWarnings("unchecked")
  T unmarshal = (T) unmarshaller.unmarshal(new StringReader(input));
  return unmarshal;
 }

 public static <T> T fromString(Class<T> clazz, InputStream input) throws JAXBException {
  JAXBContext ctx = JAXBContext.newInstance(clazz);
  @SuppressWarnings("unchecked")
  T unmarshal = (T) ctx.createUnmarshaller().unmarshal(input);
  return unmarshal;
 }
}


Запуск тестов

Половина дела сделана, теперь нам необходимо настроить инфраструктуру для тестирования наших свеженаписанных REST-сервисов. Для этого нам необходимо внести следующие модификации в pom.xml:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.11</version>

    <executions>
        <execution>
     <id>Unit tests</id>
            <phase>test</phase>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <excludes>
                    <exclude>**/IT*.java</exclude>
                </excludes>
            </configuration>
        </execution>
        <execution>
            <id>Integration tests</id>
            <phase>integration-test</phase>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <includes>
                    <include>**/IT*.java</include>
                </includes>
            </configuration>
        </execution>
    </executions>
</plugin>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.1.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-spring</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>tjws</artifactId>
    <version>${resteasy-jaxrs.version}</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <scope>test</scope>
</dependency>

Заключение

В целом, ничего сложного, главное делать всё аккуратно. Хорошего вам кода!

28 January, 2012

Немножко магии от AspectJ

Наверно, вы уже сталкивались с таким понятием, как AOП - аспектно-ориентированное программирование.

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

Но это не единственные области применения АОП.

Я хочу показать ещё пару областей применения из реальных проектов:

1. Модификация исходного кода для реализации дополнительных возможностей.
2. Принудительная проверка контракта между модулями.

Модификация исходного кода для реализации дополнительных возможностей

Предположим, что у нас есть модуль в приложении, который предоставляет нужную нам функциональность. С модулем всё в порядке, кроме одного - все его методы могут выбрасывать проверяемые исключения, что ведёт к ухудшению читаемости кода, так как вместо простого вызова метода:

service.doUsefulThing();

наш вызов превращается в

try {
        service.doUsefulThing();
    } catch ( FirstServiceException e) {
        processException(e);
    } catch ( SecondServiceException e) {
        processException(e);
    }

Дополнительная проблема в том, что у модуля количество модулей 10+, количество методов также велико, что приводит к тому, что блоки try/catch замусоривают код. Решение с использованием паттерна Callback также приведёт к замусориванию кода.

Вариант решения проблемы с использованием AOP
Решение данной проблемы было таким - а что, если используя возможности AOP трансформировать проверяемое исключение в непроверяемое? Таким образом, мы сможем избавиться от скучной проверки на исключения в нашем коде, а для обработки исключения (ведь мы всегда его будем обрабатывать одинаково) достаточно будет использовать обработку исключения на верхнем уровне абстракции.

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

package com.blogger.atamanenko;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
@Inherited
public @interface SuppressExceptions {
}
    

А также аспект, который бы делал всю нужную нам функциональность:

public aspect ExceptionSupressingAspect {

        declare soft :ServiceException:  execution(@com.blogger.atamanenko.annotation.SuppressExceptions * *.*(..));

}

Данный аспект делает в точности следующее: "Смягчает" исключение ServiceException для метода, который помечен аннотацией @SuppressExceptions.

Пример использования:

@SuppressExceptions
    protected Entity findEntity(final Identifiable id) {
        return entityService.findById(id);
    }

Принудительная проверка контракта между модулями

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

Для реализации таких проверок можно также использовать возможности AOP.

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

aspect ForbidAccessToModelAspect {

//      Full prohibition of access to model:
        pointcut accessModel(): call(* com.blogger.atamanenko.app.model..*.*(..));
        declare error: accessModel() : "Illegal call to model";

}

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

Если же нам необходимо разрешить доступ к одному пакету только из какого-то определённого другого, то мы можем модифицировать наш аспект следующим образом:

aspect ForbidAccessToModelAspect2 {

        pointcut accessModel(): call(* com.blogger.atamanenko.app.model.**.*(..));

        // Allow access to model from specific package for methods and constructors
        pointcut allowAccessModelFromSpecificPackage(): withincode(* com.blogger.atamanenko.app.allowedpackage..*.*(..));
        pointcut allowAccessModelFromSpecificPackage2(): withincode(com.blogger.atamanenko.app.allowedpackage..*.new(..));

        // forbid usage from any other methods.
        declare error: accessModel() && !(allowAccessModelFromSpecificPackage() || allowAccessModelFromSpecificPackage()):"Illegal call to Model from forbidden package";

}


Такой аспект, созданный в нашем модуле запретит нам использовать классы модели из всех пакетов, кроме com.blogger.atamanenko.app.allowedpackage

Сборка приложения

Файл аспекта нужно положить в каталог src/main/aspect, а для сборки приложения необходимо использовать не стандартный Oracle Java Compiler, а AspectJ compiler.

Пример конфигурации для Apache Maven:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>${aspectj-maven-plugin.version}</version>
    <configuration>
        <complianceLevel>1.6</complianceLevel>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
        <verbose>true</verbose>
    </configuration>
    <executions>
        <execution>
            <phase>process-sources</phase>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Заключение

Вот в общем-то и всё. Я сознательно не стал описывать языковые конструкции аспектов, так как они подробно описаны в руководстве AspectJ

21 July, 2011

Немного о виртуальных методах в Java

Сегодня я хочу рассмотреть некоторые особенности переопределения методов в Java. В java нельзя переопределить:
  • поля класса
  • конструкторы, инициализаторы класса
  • статические методы
  • статические поля

Подробнее об этом можно прочитать в Java Language Specification, §8.4.8

Итак, в java все нестатические неприватные (то есть, protected, package и public) методы являются виртуальными. Ключевое слово final запрещает возможность дальнейшего переопределения метода в подклассах. Рассмотрим следующий пример:

public class A {
     int i = 3;
     int getI() {return i;}
}

public class B extends A{
     int i = 5;
     int getI() {return i;}
}

A a = new B();
System.out.println(a.i);
System.out.println(a.getI());


Вопрос: что выведет данный код?
Ответ:
1. Так как поля класса не наследуются, то у класса A своё поле i и у класса B тоже своё поле i. Так как для полей полиморфизм не действует, то при обращении a.i мы обращаемся к классу A, поэтому на экран будет выведено 3.
2. При вызове метода a.getI() у нас в дело вступает полиморфизм, поэтому будет вызван метод от класса, инстанс которого был создан. Соответственно, мы получим на выходе 5.


Другой пример:

public class A {
     static int i = 3;
     static int getI() {return i;} 
}

public class B extends A{
     static int i = 5;
     static int getI() {return i;}
}

A a = new B();
System.out.println(a.i);
System.out.println(a.getI());


Статические поля и методы виртуальными не являются, поэтому оба вызова выведут нам 3.

06 December, 2010

Сокращаем ссылки на андроиде

Вступление
Встала передо мной задача - сокращать ссылки перед тем, как отправлять их в Twitter. Для решения этой задачи я решил использовать bit.ly, благо, их API внятный и простой.
Программируем!
Решение нарисовалось в виде следующего класса:
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;

import android.util.Log;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;


/**
 * Helper class to work with bitly.
 *
 * @author Oleg Atamanenko
 * @since 06-Dec-2010 12:49:36
 */
public class Bitly {

    private static final String TAG = "Bitly";

    private static final String SHORTEN = "/v3/shorten";
    private static final String API_URL = "api.bit.ly";
    private static final String RESPONSE_FORMAT = "json";

    private String username;
    private String apiKey;


    public Bitly(String username, String apiKey) {
        this.username = username;
        this.apiKey = apiKey;
    }

    public String shorten(String longUrl) throws BitlyException {
        DefaultHttpClient httpClient = new DefaultHttpClient();

        try {
            List params = new ArrayList();
            params.add(new BasicNameValuePair("login", username));
            params.add(new BasicNameValuePair("apiKey", apiKey));
            params.add(new BasicNameValuePair("longUrl", longUrl));
            params.add(new BasicNameValuePair("format", RESPONSE_FORMAT));


            URI uri = URIUtils.createURI("http", API_URL, -1, SHORTEN, URLEncodedUtils.format(params, "UTF-8"), null);
            HttpGet request = new HttpGet(uri);

            Log.d(TAG, "Sending request: " + request.getURI());

            HttpResponse httpResponse = httpClient.execute(request);

            HttpEntity httpEntity = httpResponse.getEntity();
            String response = EntityUtils.toString(httpEntity);
            Log.i(TAG, "Bitly response is: " + response);
            httpResponse.getEntity().consumeContent();

            JSONObject jsonResponse = new JSONObject(response);

            checkForExceptions(jsonResponse);

            JSONObject data = jsonResponse.getJSONObject("data");
            return data.getString("url");

        } catch (ClientProtocolException e) {
            throw new BitlyException(e);
        } catch (IOException e) {
            throw new BitlyException(e);
        } catch (JSONException e) {
            throw new BitlyException(e);
        } catch (URISyntaxException e) {
            throw new BitlyException(e);
        }
    }

    private void checkForExceptions(JSONObject jsonResponse) throws JSONException, BitlyException {
        int statusCode = jsonResponse.getInt("status_code");
        if (statusCode != 200) {
            String message = jsonResponse.getString("status_txt");
            throw new BitlyException(message);
        }
    }

}
Конструктор класс принимает на вход следующие параметры: Полная документация к Bit.ly API расположена на отдельном проекте в Google Code
Использование
Единственный метод, реализованный сейчас - это метод shorten(). На вход требуется подать полную ссылку longUrl, на выходе получается укороченная версия ссылки, либо кидается исключение с сообщением от bit.ly API.
Пример вызова
Bitly bitly = new Bitly(BITLY_USERNAME, BITLY_API_KEY);
String shortLink = bitly.shorten(link);

Дальнейшие улучшения
Если перед вами стоит задача быстренько сократить ссылку - то вышеприведённого кода достаточно. Но если вам нужно полноценное решение, со всеми возможностями bit.ly - то посмотрите в сторону bitlyj. Правда, я не уверен, что оно взведётся под андроидом.

22 May, 2010

Удаление различных диакритических символов из строки

Возникла проблема - каким образом заменить в строке символы из национальных кодировок на соответствующие им из латиницы.

Например, из строки explicación получить explicacion.

package com.blogspot.atamanenko;

import java.text.Normalizer;
import java.text.Normalizer.Form;

public class StringNormalizer {

    public static String normalize(String string) {
        return Normalizer.normalize(string, Form.NFD)
            .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
    }
}

Вызов Normalizer.normalize проводит нормализацию входной строки. Последующий вызов регулярного выражения удаляет все диакритические знаки, полученные после нормализации.

Создание больших объёмов тестовых данных с помощью Databene Benerator

Периодически необходимо решать задачу создания больших ( и не очень) объёмов тестовых данных для проведения различных видов тестирования - функционального, нагрузочного (тестирование стабильности и производительности). При этом часто получается так, что система на тестовых данных ведёт себя совсем иначе, чем на реальных данных. Причина кроется в том, что создать правдоподобные тестовые данные всегда достаточно сложно.

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

Установка

Установка фреймворка осуществляется двумя способами - как отдельное приложение и как плагин для Maven.

Установка под Maven

Для использования Databene benerator в проектах, использующих для сборки Apache Maven необходимо добавить в зависимости databene-benerator и сконфигурировать его.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.myorganization</groupId>
  <artifactId>databene-benerator-test</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <name>data generation project</name>
  <dependencies>
    <dependency>
      <groupId>org.databene</groupId>
      <artifactId>databene-benerator</artifactId>
      <version>0.5.9</version>
    </dependency>
    <dependency>
      <groupId>org.databene</groupId>
      <artifactId>databene-webdecs</artifactId>
      <version>0.4.9</version>
    </dependency>
    <dependency>
      <groupId>org.databene</groupId>
      <artifactId>databene-commons</artifactId>
      <version>0.4.9</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
  </dependencies>
  <build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <encoding>UTF-8</encoding>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.databene</groupId>
        <artifactId>maven-benerator-plugin</artifactId>
        <version>0.5.9</version>
        <executions>
          <execution>
            <phase>compile</phase>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <descriptor>src/main/resources/benerator.ben.xml</descriptor>
          <encoding>UTF-8</encoding>
          <validate>true</validate>
          <dbUrl>jdbc:mysql://localhost:3306/hrtool?useUnicode=true&characterEncoding=UTF-8</dbUrl>
          <dbDriver>com.mysql.jdbc.Driver</dbDriver>
          <dbSchema>database</dbSchema>
          <dbUser>user</dbUser>
          <dbPassword>password</dbPassword>
        </configuration>
        <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
            <scope>runtime</scope>
          </dependency>
          <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.5-beta5</version>
            <scope>runtime</scope>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>
</project>

В конфигурации необходимо указать как минимум следующие параметры:

  1. dbUrl - строка подключения к базе данных в формате JDBC
  2. dbDriver - используемый драйвер базы данных
  3. dbSchema - имя схемы базы данных
  4. dbUser - имя пользователя, под которым подключаемся к базе данных
  5. dbPassword - пароль пользователя для подключения к базе данных

Кроме использования файла pom.xml databene поддерживает возможность указания параметров в файле benerator.properties, что может быть удобно.

Databene benerator обладает следующими возможностями:

  1. Решение проблемы создания данных в общем виде. На данный момент поддерживаются XML и реляционные базы данных, но не за горами поддержка веб-сервисов, SAP и любых других систем через механизм расширений.
  2. Юзабилити. Databene-benerator позволяет упростить создание тестовых данных для сложной модели прикладной области.
  3. Обработка больших объёмов данных.
  4. Высокая производительность.
  5. Поддержка доменных областей.
  6. Качество данных. Фреймворк поддерживает проверку ограничений модели прикладной области.
  7. Компонентная, легкорасширяемая архитектура.
  8. Широкие возможности изменения и настройки генерации тестовых данных
  9. Создание тестовых данных с нуля.
  10. Импорт и анонимизация реальных данных.

В комплекте идёт толковая, но, к сожалению, неполная документация по возможностям.

11 April, 2010

Несколько слов о GORM

В данной заметке хочу поделиться некоторыми моментами использования GORM.

GORM - это ORM-фреймворк, используемый в Grails. Реализован он поверх Hibernate, но, при этом, с некоторыми отличными умолчаниями.

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

Маппинг один-ко-многим

По умолчанию GORM для связей один ко многим (one-to-many) создаёт таблицу-связку, которая обычно нужна только при связях между сущностями вида многие ко многим. Чтобы исправить это поведение необходимо указать GORM, чтобы он не создавал таблицу связку.

class Person implements Serializable {
  static hasMany = [
    scores: ScoreSheet
  ]
  
  static mapping = {
    scores joinTable: false
  };
}

Использование однонаправленных связей

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

class Note implements Serializable {
  static belongsTo = [
    person: Person
  ]
}

class Person implements Serializable {
  // person fields.
}

Для работы с Notes необходимо использовать такие запросы:

  Note.findByPerson(person).each { -> };

вместо

  person.notes.each { -> }

Маппинг иерархии классов доменных сущностей

GORM поддерживает только два варианта маппинга иерархии классов, в отличии от Hibernate: Таблица на всю иерархию (table-per-hierarchy), или таблица на каждый подкласс (table-per-subclass). У маппинга table-per-hierarchy есть серьёзный недостаток - подклассы не могут иметь ненулевые поля. Поэтому, если этот недостаток критичен, то необходимо использовать маппинг table-per-subclass.

class Payment {
  Long id
  Long version
  Integer amount

  static mapping = {
    tablePerHierarchy false
  }
}

class CreditCardPayment extends Payment {
  String cardNumber
}
 

22 March, 2010

Общение со Skype через D-Bus на Python

Summary: в данной заметке описывается работа с программой Skype через D-Bus на Python.

Введение

Захотелось мне странного - когда я ухожу домой, мне нужно выключить amarok, kopete и Skype. Собственно, решено было через D-Bus отправлять вышеперечисленным приложениям релевантные сообщения.

Используем dbus-send

Сначала я использовал обычный dbus-send, что оформилось в виде следующего скрипта go2home:

#!/bin/sh

# Stop amarok
dbus-send --session --type=method_call --dest=org.kde.amarok /Player org.freedesktop.MediaPlayer.Stop

# Logout from kopete
dbus-send --session --type=method_call --dest=org.kde.kopete /Kopete org.kde.Kopete.disconnectAll 

# Logout from Skype
skypeapi.py 'SET USERSTATUS OFFLINE'

# Lock screen
dbus-send --session --type=method_call --dest=org.freedesktop.ScreenSaver /ScreenSaver org.freedesktop.ScreenSaver.Lock
Детали и параметры работы команды dbus-send описаны в man-странице

Проблема со скайпом

Со скайпом пришлось немного повозиться, так как для работы с ним необходима постоянная сессия, что нельзя сделать с помощью dbus-send.

Прочитав описание протокола на сайте

был создан нижеследующий скрипт: skypeapi.py

#!/usr/bin/env python
import dbus, sys

def main():
    remote_bus = dbus.SessionBus()
    
    # Check if skype is running.
    system_service_list = remote_bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus').ListNames()
    skype_api_found = 0
    for service in system_service_list:
        if service=='com.Skype.API':
            skype_api_found = 1
            break
    if not skype_api_found:
        sys.exit('No running API-capable Skype found')

    # Get skype dbus api
    skype_service = remote_bus.get_object('com.Skype.API', '/com/Skype')

    # Connect to skype.
    answer = skype_service.Invoke('NAME SkypeApiClient')
    if answer != 'OK':
        sys.exit('Could not bind to Skype client')

    # Check if protocol is supported.
    answer = skype_service.Invoke('PROTOCOL 1')
    if answer != 'PROTOCOL 1':
        sys.exit('This test program only supports Skype API protocol version 1')

    # Invoke operations
    for arg in sys.argv:
        skype_service.Invoke(arg)
    
    return 0    

if __name__ == "__main__":
    main()

При первом запуске скрипта появится скайповский диалог с вопросом, можно ли разрешить приложению доступ к скайпу. После нажатия на "Да" Skype добавит наш скрипт в разрешённые и мы сможем управлять скайпом.

После этого для скрипта go2home был создана иконка на панели.

Заключение

Как видно, работа с DBus из Python проста и элегантна. В качестве домашнего упражнения предлагаю написать скрипт, который после разблокирования экрана будет запускать все нужные приложения.

21 March, 2010

Разработка макроса для TiddlyWiki

Summary: Пример разработки плагина для TiddlyWiki

Вступление

TiddlyWiki - это вики-движок, полностью написанный на JavaScript и хранящийся в одном файле (как сам движок, так и содержимое). Создатели позиционируют его как "переиспользуемую нелинейную персональную веб записную книжку".

Я давно использую TiddlyWiki в различных целях:

  1. По прямому назначению.
  2. Как систему GTD.
  3. Как домашнюю страницу на компьютере.

Моя домашняя страница на компьютере - это, если говорить терминами TiddlyWiki, тиддлер, содержащий ссылки на страницы, которые я часто посещаю.

Для удобства использования я разработал пару стилей CSS, так что ссылки были крупными.

Исходный код плагина

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

/*{{{*/
version.extensions.faviconLinkMacro = {major: 0, minor: 1, revision: 0, date: new Date(2010,3,21)};
// Author: Oleg Atamanenko
config.macros.faviconLink = {}
config.macros.faviconLink.handler = function(place, macroName,  params, wikifier, paramString) {
  var linkBox = createTiddlyElement(place, "span", null, "favIcon", "");

  var args = paramString.parseParams("list",null,true);
  var link = getParam(args, "link", 'false');
  
  if (link != 'false'){
    urlParts = link.split('/');
    imgLink = urlParts[0] + "//" + urlParts[2] + "/favicon.ico";

    var imgElement = createTiddlyElement(linkBox, "img", null, "faviconImage", "");
    imgElement.src = imgLink;
    imgElement.width = 16;
    imgElement.height = 16;

    var linkTitle = getParam(args, "title", 'false');

    if(linkTitle == 'false'){
      linkTitle = link;
    }
    var linkElement = createTiddlyElement(linkBox, "a", null, "faviconLink", linkTitle);
    linkElement.href = link;
    linkElement.target = '_blank';
  }
}

/*}}}*/

Установка плагина

  1. Создаём новый тиддлер, называем его faviconLinkMacro
  2. Помечаем тиддлер тегом systemConfig, тогда TiddlyWiki при открытии страницы его подгрузит.

Использование плагина

  1. Плагин создаёт новый макрос, faviconLink, с двумя параметрами link и title.
  2. Вызов <<faviconLink link:'https://mail.google.com/mail' title:'mail'>> создаёт ссылку с картинкой с GMail

Настройка визульных стилей плагина

Макрос создаёт следующие классы:

  1. span.favIcon
  2. img.faviconImage
  3. a.faviconLink

Пример стилей

.links span {
  display: block;
  position: relative;
  padding: 0.7em;
  margin: 0.3em;
  min-width: 6em;
  float:left;
  text-align: center;
  font-weight: bold;
  font-size: 1.5em;
  font-family: "Gill Sans MT", "Candara", "Arial"; 
  border: 2px solid [[ColorPalette::SecondaryLight]];
  background-color: [[ColorPalette::SecondaryPale]];
}

.links span:hover {
  border: 2px solid [[ColorPalette::SecondaryMid]];
  background-color: [[ColorPalette::SecondaryLight]];
  color: [[ColorPalette::PrimaryMid]];
}

.links img.faviconImage {
  position: relative;
  padding-right: 10px;
}

Данный CSS необходимо добавить в тиддлер с именем StyleSheet

Пример тиддлера со ссылками

{{links{
<<faviconLink link:'http://delicious.com/dark.schakal/2read' title:'2read'>> <<faviconLink link:'https://mail.google.com/mail' title:'mail'>> <<faviconLink link:'http://www.google.com/reader/view' title:'rss'>> <<faviconLink link:'http://atamanenko.blogspot.com/' title:'blog'>> <<faviconLink link:'http://feedburner.google.com/fb/a/myfeeds' title:'feedburner'>> <<faviconLink link:'https://www.google.com/analytics/settings/' title:'analytics'>> <<faviconLink link:'http://wave.google.com' title:'wave'>> <<faviconLink link:'http://www.blogger.com/home' title:'blogger'>> <<faviconLink link:'http://twitter.com/' title:'twitter'>> <<faviconLink link:'https://www.dropbox.com/home#/' title:'dropbox'>> <<faviconLink link:'http://translate.google.com/toolkit/' title:'translator'>> <<faviconLink link:'http://sibir.megafon.ru/sendsms/' title:'SendSMS'>>
}}}
Вышеприведённый код тиддлера создаёт следующую страницу: