среда, июля 14, 2010

Apache Cassandra и Spring MessageSource

Если Вы в Spring MVC приложении хотите использовать Apache Cassandra как персистентное хранилище строковых сообщений при интернационализации приложения, может быть полезной представленная далее реализация AbstractMessageSource. К особенностям ее работы стоит отнести кеширование ресурсов на старте и кеширование отсутствующих в кеше значений, затребованных во время выполнения. В качестве клиента Cassandra используется Hector.
Итак, допустим у нас есть следующие декларации в конфиге Cassandra:
<Keyspace Name="Keyspace1">
<ColumnFamily Name="i18n" CompareWith="UTF8Type"/> 
...
</Keyspace>
По сути мы всего лишь объявили новую ColumnFamily, в которой в строках будут наименования строк сообщений, в столбцах значения. Наименование столбца должно соответствовать языку локали (locale.getLanguage()).
ColumnFamily:i18n
|-----------------------------------------------------------------------
|resourceName\Locale|        en     |       ru       |       de        |
|-----------------------------------------------------------------------
|hello              |      Hello    |     Привет     |     Hallo       |
|resourceStringN    |ResourceStringN|РесурснаяСтрокаN|RessourcenStringN|
|-----------------------------------------------------------------------

Далее совершенно типичная конфигурация в Spring с использованием Cassandra и Hector:

<!-- Configures MessageSource -->
<bean id="messageSource" class="com.applusion.cassadraspring.CassandraResourceBundleMessageSource">
  <property name="cassandraClientPool"  ref="cassandraClientPool"/>
  <property name="keyspace" ref="keyspace"/>
  <property name="columnFamilyName" value="i18n"/>
</bean>
<!-- Configures keyspace name -->
<bean id="keyspace" class="java.lang.String">
  <constructor-arg><value>Keyspace1</value></constructor-arg>
</bean>
    
<bean id="cassandraClientMonitor" class="me.prettyprint.cassandra.service.CassandraClientMonitor"/>

<bean id="jmxMonitor" class="me.prettyprint.cassandra.service.JmxMonitor" factory-method="getInstance"/>

<bean id="cassandraClientPoolFactory" class="me.prettyprint.cassandra.service.CassandraClientPoolFactory" factory-method="getInstance"/>

<bean id="cassandraClientPool" factory-bean="cassandraClientPoolFactory" factory-method="createNew" >
        <constructor-arg><ref bean="cassandraHostConfigurator"/></constructor-arg>
</bean>

<bean id="cassandraHostConfigurator" class="me.prettyprint.cassandra.service.CassandraHostConfigurator">
<constructor-arg value="localhost:9160"/>
</bean>

Все что снизу относится к Hector, чуть выше объявление keyspace как строки с названием кейспейса, и, собственно, бин messageSource для которого определены пул соединений с Cassandra, кейспейс и наименование ColumnFamily, в котором хранятся ресурсы.

Сам класс:
/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.applusion.cassadraspring;

import static me.prettyprint.cassandra.utils.StringUtils.bytes;
import static me.prettyprint.cassandra.utils.StringUtils.string;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import me.prettyprint.cassandra.dao.SpringCommand;
import me.prettyprint.cassandra.service.CassandraClient;
import me.prettyprint.cassandra.service.CassandraClientPool;
import me.prettyprint.cassandra.service.Keyspace;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ColumnPath;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.KeyRange;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.AbstractMessageSource;

/**
 * MessageSource implementation that reads the messages from a Apache Cassandra.
 * 
 * @author Oleg Smith
 */

public class CassandraResourceBundleMessageSource extends AbstractMessageSource
  implements InitializingBean {

 private CassandraClientPool cassandraClientPool;
 private String keyspace = "Keyspace1";
 private String columnFamilyName = "Standard2";
 private ConsistencyLevel consistencyLevel = CassandraClient.DEFAULT_CONSISTENCY_LEVEL;

 /**
  * Local cache. Universal Map<String, Map<String, String>>
  */
 private Map<String, Map<String, String>> cache = new HashMap<String, Map<String, String>>();

 public CassandraResourceBundleMessageSource() {
 }

 /**
  * resolveCode implementation
  */

 @Override
 protected MessageFormat resolveCode(String code, Locale locale) {
  MessageFormat codeInCache;
  if ((codeInCache = resolveCodeInCache(code, locale)) != null) {
   return codeInCache;
  } else {
   return resolveCodeInCassandra(code, locale);
  }
 }

 /**
  * Resolve code in local cache.
  */

 private MessageFormat resolveCodeInCache(String code, Locale locale) {
  Map<String, String> cacheValues = cache.get(code);
  if (cacheValues != null) {
   String cacheValue = cacheValues.get(locale.getLanguage());
   if (cacheValue != null)
    return new MessageFormat(cacheValue, locale);
   cacheValue = cacheValues.get("en");
   if (cacheValue != null)
    return new MessageFormat(cacheValue, locale);
  }
  return null;
 }

 /**
  * Resolve code in Cassandra.
  */

 private MessageFormat resolveCodeInCassandra(String code, Locale locale) {
  List<Column> columns;
  try {
   columns = getColumnsByKey(code);
   Map<String, String> cacheValues = new HashMap<String, String>();
   for (Column columni : columns) {
    cacheValues.put(string(columni.name), string(columni.value));
   }

   cache.put(code, cacheValues);

   if (cacheValues.get(locale.getLanguage()) != null)
    return new MessageFormat(cacheValues.get(locale.getLanguage()),
      locale);
   if (cacheValues.get("en") != null)
    return new MessageFormat(cacheValues.get("en"), locale);

  } catch (Exception e) {
   e.printStackTrace();
  }
  ;
  return null;
 }

 @Override
 public void afterPropertiesSet() throws Exception {
  loadAll();
 }

 /**
  * Load all strings from Cassandra repeatedly.
  */

 private void loadAll() {
  Map<String, List<Column>> keys;
  try {
   keys = getColumnsByKeys("", 1000);
   boolean isFirstIteration = true;

   while (keys.size() != 0 && keys.size() != 1) {
    Iterator<String> it = keys.keySet().iterator();
    String key = null;
    if (!isFirstIteration)
     it.next();
    while (it.hasNext()) {
     key = it.next();
     List<Column> columns = (List<Column>) keys.get(key);
     Map<String, String> columnsValues = new HashMap<String, String>();
     for (Column columni : columns) {
      columnsValues.put(string(columni.name),
        string(columni.value));
     }
     cache.put(key, columnsValues);
    }
    if (key != null)
     keys = getColumnsByKeys(key, 1000);
    isFirstIteration = false;
   }

  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 /**
  * Get 1500 columns and keyCount keys
  */

 private Map<String, List<Column>> getColumnsByKeys(final String keyStart,
   final int keyCount) throws Exception {
  return execute(new SpringCommand<Map<String, List<Column>>>(
    cassandraClientPool) {
   public Map<String, List<Column>> execute(final Keyspace ks)
     throws Exception {
    ColumnParent clp = new ColumnParent(columnFamilyName);
    SliceRange sr = new SliceRange(new byte[0], new byte[0], false,
      1500);
    SlicePredicate sp = new SlicePredicate();
    sp.setSlice_range(sr);

    KeyRange range = new KeyRange(keyCount);
    range.setStart_key(keyStart);
    range.setEnd_key("");

    Map<String, List<Column>> keys = ks.getRangeSlices(clp, sp,
      range);
    return keys;
   }
  });
 }

 /**
  * Get 1500 columns from given key
  */

 public List<Column> getColumnsByKey(final String key) throws Exception {
  return execute(new SpringCommand<List<Column>>(cassandraClientPool) {
   public List<Column> execute(final Keyspace ks) throws Exception {
    try {
     ColumnParent clp = new ColumnParent(columnFamilyName);
     SliceRange sr = new SliceRange(new byte[0], new byte[0],
       false, 1500);
     SlicePredicate sp = new SlicePredicate();
     sp.setSlice_range(sr);
     List<Column> cols = ks.getSlice(key, clp, sp);

     return cols;
    } catch (NotFoundException e) {
     return null;
    }
   }
  });
 }

 protected ColumnPath createColumnPath(String columnName) {
  return new ColumnPath(columnFamilyName).setColumn(bytes(columnName));
 }

 protected <T> T execute(SpringCommand<T> command) throws Exception {
  CassandraClient c = cassandraClientPool.borrowClient();
  Keyspace ks = c.getKeyspace(keyspace, consistencyLevel);
  try {
   return command.execute(ks);
  } finally {
   cassandraClientPool.releaseClient(ks.getClient());
  }
 }

 public CassandraClientPool getCassandraClientPool() {
  return cassandraClientPool;
 }

 public void setCassandraClientPool(CassandraClientPool cassandraClientPool) {
  this.cassandraClientPool = cassandraClientPool;
 }

 public String getKeyspace() {
  return keyspace;
 }

 public void setKeyspace(String keyspace) {
  this.keyspace = keyspace;
 }

 public String getColumnFamilyName() {
  return columnFamilyName;
 }

 public void setColumnFamilyName(String columnFamilyName) {
  this.columnFamilyName = columnFamilyName;
 }

}

Осталось закачать в Cassandra сообщения программно или иным способом. Например, с помощью консольного клиента cassandra-cli это выглядело бы так:
set Keyspace1.i18n['hello']['en'] ='Hello'
set Keyspace1.i18n['hello']['ru'] ='Привет'
set Keyspace1.i18n['hello']['de'] ='Hallo'


Теперь, если в Cassandra в jsp написать
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

то вы увидите свое интернациональное сообщение.

При интернационализации не забудьте определить обработчик локали в Spring:
<mvc:interceptors>
  <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
 </mvc:interceptors>

Комментариев нет:

Мой список блогов