пятница, июля 16, 2010

Apache Cassandra и YAML

Иногда бывает необходимо быстро загрузить в базу или выгрузить из базы небольшой объем данных в файл. Например, строки для интернационализации приложения. В последнее время в качестве контейнера для сериализованных данных активно используется YAML. Андрей Сомов делает замечательный проект SnakeYAML для использования YAML в java. Там реализован лаконичный, с возможностями настройки, (де)сериализатор бинов, коллекций и всего прочего. Именно им я пользуюсь в случае работы с YAML.
Напомню, что любой key-value DB, и Кассандра в частности является ни чем иным, как Map<String,Map<String,String>> при строковом ключе. Это вполне позволяет сделать универсальный загрузчик данных, который и будет представлен далее.

Интерфейс
/*
 * 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 for 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;
 }

}

Настройка Spring
<bean id="backup" class="com.applusion.cassadraspring.CassandraYAMLLoader">
  <property name="cassandraClientPool"  ref="cassandraClientPool"/>
  <property name="keyspace" ref="keyspace"/> 
 </bean>

<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>

Ну все, теперь остается только вызвать методы инжектированного backup. Например backup.LoadYAML("dbyaml/yaml.yml", "i18n") загрузит данные из файла dbyaml/yaml.yml в ColumnFamily i18n в Keyspace1, а backup.SaveYAML("dbyaml/yaml.yml", "i18n") сохранит.

формат yml будет какой-то такой:
cancel: {en: Cancel, ru: Отмена}
save: {en: Save, ru: Сохранить}
from: {en: From, ru: От}
password: {en: Password, ru: Пароль}
time: {en: Time, ru: Время}
timezone: {en: TimeZone, ru: Часовой пояс}
more: {en: Next 20, ru: Следующие 20}
search: {en: Search, ru: Поиск}

четверг, июля 15, 2010

Apache Cassandra и Spring Security UserDetailsService

Если Вы в Spring MVC приложении хотите использовать Apache Cassandra как персистентное хранилище информации о пользователях и использовать ее при разграничении доступа, может быть полезной представленная далее реализация UserDetailsService. В качестве клиента Cassandra используется Hector.
Допустим у нас есть следующие декларации в конфиге Cassandra:
<Keyspace Name="Keyspace1">
<ColumnFamily Name="Configuration" CompareWith="UTF8Type"/> 
<ColumnFamily Name="Users" CompareWith="UTF8Type"/>
...
</Keyspace>
Мы объявили две ColumnFamily. В Users будем хранить записи пользователей, в Configuration общие настройки приложения, в частности в одной предопределенной записи, скажем с именем "GroupsAuthority", в столбцах будет определение групп и прав.

ColumnFamily:Users
|--------------------------------------------------------------------------
|UserName\Property| enabled | group      |           password (MD5)       |
|--------------------------------------------------------------------------
|user1            |  true   | user,admin |24c9e15e52afc47c225b757e7bee1f9d|
|userN            |  false  | editor     |362edb266f062db3ddbd1098b8affcbd|
|--------------------------------------------------------------------------

ColumnFamily:Configuration
|-------------------------------------------------------------------
|               |   user   | editor   |           admin            |
|-------------------------------------------------------------------
|GroupsAuthority| canView  | canEdit  | canView, canEdit, canBackup|
|-------------------------------------------------------------------

Далее необходимая конфигурация в Spring
<authentication-manager>
    <authentication-provider user-service-ref="userServiceBean">
        <password-encoder hash="md5"/>                                 
    </authentication-provider>
</authentication-manager>

<beans:bean id="userServiceBean" class="com.applusion.cassadraspring.CassandraHectorUserDetailsServiceImpl">
 <beans:property name="cassandraClientPool" ref="cassandraClientPool"/>
 <beans:property name="keyspace" value="Keyspace1"/>
 <beans:property name="usersCF" value="Users"/>
 <beans:property name="passwordColumn" value="password"/>
 <beans:property name="enabledColumn" value="enabled"/>
 <beans:property name="groupColumn" value="group"/>
 <beans:property name="configurationCF" value="Configuration"/>
 <beans:property name="groupsAuthorityKey" value="GroupsAuthority"/>
</beans:bean>

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

    
<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>

Сам класс:
/*
 * 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 java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
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.ConsistencyLevel;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * UserDetailsService implementation that reads user details from a Apache Cassandra.
 * 
 * @author Oleg Smith
 */

public class CassandraHectorUserDetailsServiceImpl implements UserDetailsService {
 private CassandraClientPool cassandraClientPool;
 private String keyspace = "Keyspace1";
 private String usersCF = "Standard2";
 private String passwordColumn = "password";
 private String enabledColumn = "enabled";
 private String groupColumn = "group";
 private String configurationCF="Configuration";
 private String groupsAuthorityKey="GroupsAuthority";

 
 private ConsistencyLevel consistencyLevel = CassandraClient.DEFAULT_CONSISTENCY_LEVEL;

 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 
 public CassandraHectorUserDetailsServiceImpl() {
   } 
 
 public CassandraHectorUserDetailsServiceImpl(CassandraClientPool cassandraClientPool) {
     this.cassandraClientPool = cassandraClientPool;
   }
 
 @Override
 public UserDetails loadUserByUsername(String username)
   throws UsernameNotFoundException, DataAccessException {
  List<Column> cl, cl2;
  
  try {
   cl = getUserProperties(username);
   if (cl.size()==0) {
     throw new UsernameNotFoundException(
                      messages.getMessage("CassandraHectorUserDetailsServiceImpl.notFound", new Object[]{username}, "Username {0} not found"), username);   
   }
   
   cl2 = getGroupsAuthority();
   if (cl2.size()==0) {
     throw new UsernameNotFoundException(
                      messages.getMessage("CassandraHectorUserDetailsServiceImpl.notFound", new Object[]{username}, "Not found any GroupsAuthority in base"));   
   }
      
   Map<String,String> userProperties=new HashMap<String,String>();
   for (Column cli:cl) {
    userProperties.put(new String(cli.name),new String(cli.value));
   }
   String[] userGroups=userProperties.get(groupColumn).split(",");
   String password=userProperties.get(passwordColumn);
   boolean enabled=userProperties.get(enabledColumn).trim().equals("true") ? true: false;
   
   
   Map<String,String> groupsAuthority=new HashMap<String,String>();
   for (Column cli:cl2) {
    groupsAuthority.put(new String(cli.name),new String(cli.value));
   }

   List<String> authorities=new ArrayList<String>(); 
   
   for (String userGroup:userGroups) {
    String[] gauthorities=groupsAuthority.get(userGroup).split(","); 
    for (String authority:gauthorities) {
     authorities.add(authority); 
    }    
   }
      
   Collection<GrantedAuthority> dbAuths = new ArrayList<GrantedAuthority>();
   
   for (String auth:authorities) {
    if (!auth.equals("")) dbAuths.add(new GrantedAuthorityImpl(auth.trim()));
   }
   
   return new User(username, password, enabled, true,true, true, dbAuths);
   

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

  return null;
 }
 
 
   public List<Column> getUserProperties(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(usersCF);           
              SliceRange sr = new SliceRange(new byte[0], new byte[0], false, 150);
              SlicePredicate sp = new SlicePredicate();
              sp.setSlice_range(sr);
              List<Column> cols = ks.getSlice(key, clp, sp);           
           return cols;
          } catch (NotFoundException e) {
            return null;
          }
        }
      });
    } 
 
   public List<Column> getGroupsAuthority() throws Exception {
      return execute(new SpringCommand<List<Column>>(cassandraClientPool){
        public List<Column> execute(final Keyspace ks) throws Exception {
          try {
           ColumnParent clp = new ColumnParent(configurationCF);           
              SliceRange sr = new SliceRange(new byte[0], new byte[0], false, 150);
              SlicePredicate sp = new SlicePredicate();
              sp.setSlice_range(sr);
              List<Column> cols = ks.getSlice(groupsAuthorityKey, clp, sp);           
           return cols;
          } catch (NotFoundException e) {
            return null;
          }
        }
      });
    }  
   
   
  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 getUsersCF() {
  return usersCF;
 }

 public void setUsersCF(String usersCF) {
  this.usersCF = usersCF;
 }

 public String getPasswordColumn() {
  return passwordColumn;
 }

 public void setPasswordColumn(String passwordColumn) {
  this.passwordColumn = passwordColumn;
 }

 public String getEnabledColumn() {
  return enabledColumn;
 }

 public void setEnabledColumn(String enabledColumn) {
  this.enabledColumn = enabledColumn;
 }

 public String getGroupColumn() {
  return groupColumn;
 }

 public void setGroupColumn(String groupColumn) {
  this.groupColumn = groupColumn;
 }

 public String getConfigurationCF() {
  return configurationCF;
 }

 public void setConfigurationCF(String configurationCF) {
  this.configurationCF = configurationCF;
 }

 public String getGroupsAuthorityKey() {
  return groupsAuthorityKey;
 }

 public void setGroupsAuthorityKey(String groupsAuthorityKey) {
  this.groupsAuthorityKey = groupsAuthorityKey;
 }

 public ConsistencyLevel getConsistencyLevel() {
  return consistencyLevel;
 }

 public void setConsistencyLevel(ConsistencyLevel consistencyLevel) {
  this.consistencyLevel = consistencyLevel;
 }
      
}

Вроде все. Теперь в контроллере можно аннотациями ограничивать права. Например так PreAuthorize("hasRole('canBackup')")

среда, июля 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>

понедельник, июля 12, 2010

Eclipse, UrlRewriteFilter 3.2 и Referenced file contains errors http://tuckey.org/res/dtds/urlrewrite3.2.dtd

Иногда при открытии проекта Spring MVC с использованием UrlRewriteFilter 3.2 Eclipse ругается ошибкой "Referenced file contains errors (http://tuckey.org/res/dtds/urlrewrite3.2.dtd). For more information, right click on the message in the Problems View and select "Show Details...". Не очень понятно в чем конкретно дело, возможно что Eclipse как-то не до конца скачивает схему, но решить проблему помогает отчистка кеша Eclipse. Делается это через меню Window/Preferences далее закладки General/Network Connections/Cache. Удаляете в списке закешированных сетевых ресурсов все urlrewrite3.X.dtd в надежде на то, что в следующий раз закешируется валидная схема. Если и это не поможет - помещайте схему в папку WEB-INF.

Линк по теме:
Error while creating a Spring MVC project in STS

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