/* This file is part of Cyclos (www.cyclos.org). A project of the Social Trade Organisation (www.socialtrade.org). Cyclos is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Cyclos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Cyclos; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package nl.strohalm.cyclos.services.customization; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import nl.strohalm.cyclos.dao.customizations.TranslationMessageDAO; import nl.strohalm.cyclos.entities.customization.translationMessages.TranslationMessage; import nl.strohalm.cyclos.entities.customization.translationMessages.TranslationMessageQuery; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.settings.LocalSettings; import nl.strohalm.cyclos.services.InitializingService; import nl.strohalm.cyclos.services.fetch.FetchServiceLocal; import nl.strohalm.cyclos.services.settings.SettingsServiceLocal; import nl.strohalm.cyclos.utils.CacheCleaner; import nl.strohalm.cyclos.utils.DataIteratorHelper; import nl.strohalm.cyclos.utils.TransactionHelper; import nl.strohalm.cyclos.utils.cache.Cache; import nl.strohalm.cyclos.utils.cache.CacheAdapter; import nl.strohalm.cyclos.utils.cache.CacheCallback; import nl.strohalm.cyclos.utils.cache.CacheManager; import nl.strohalm.cyclos.utils.conversion.LocaleConverter; import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData; import nl.strohalm.cyclos.utils.transaction.TransactionCommitListener; import nl.strohalm.cyclos.utils.validation.Validator; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; /** * Implementation for message service * @author luis */ public class TranslationMessageServiceImpl implements TranslationMessageServiceLocal, InitializingBean, InitializingService { private static final String PROPERTIES_KEY = "_PROPERTIES"; private TranslationMessageDAO translationMessageDao; private FetchServiceLocal fetchService; private CacheManager cacheManager; private SettingsServiceLocal settingsService; private TransactionHelper transactionHelper; private final List<TranslationChangeListener> listeners = new ArrayList<TranslationChangeListener>(); @Override public void addTranslationChangeListener(final TranslationChangeListener listener) { listeners.add(listener); } @Override public void afterPropertiesSet() throws Exception { getCache().addListener(new CacheAdapter() { @Override public void onCacheCleared(final Cache cache) { transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { notifyListeners(exportAsProperties()); } }); } }); } @Override public synchronized Properties exportAsProperties() { return getCache().get(PROPERTIES_KEY, new CacheCallback() { @Override public Object retrieve() { return translationMessageDao.listAsProperties(); } }); } @Override public void importFromProperties(final Properties properties, MessageImportType importType) { // Delete all messages if we will replace with the new file if (importType == MessageImportType.REPLACE) { translationMessageDao.deleteAll(); importType = MessageImportType.ONLY_NEW; } if (importType == MessageImportType.ONLY_NEW) { importOnlyNewProperties(properties); } else { final boolean emptyOnly = importType == MessageImportType.NEW_AND_EMPTY; importNewAndModifiedProperties(properties, emptyOnly); } clearCacheOnCommit(); } @Override public void initializeService() { final LocalSettings localSettings = settingsService.getLocalSettings(); Properties properties = readFile(localSettings.getLocale()); importFromProperties(properties, MessageImportType.NEW_AND_EMPTY); } @Override public TranslationMessage load(final Long id) { return translationMessageDao.load(id); } @Override public Properties readFile(final Locale locale) { final String language = LocaleConverter.instance().toString(locale); final String propertiesName = "ApplicationResources_" + language + ".properties"; InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("/" + propertiesName); final Properties properties = new Properties(); try { final Reader reader = new InputStreamReader(in, "UTF-8"); properties.load(reader); } catch (final Exception e) { // Ignore } finally { IOUtils.closeQuietly(in); } return properties; } @Override public int remove(final Long... ids) { final int count = translationMessageDao.delete(ids); clearCacheOnCommit(); return count; } @Override public TranslationMessage save(TranslationMessage translationMessage) { validate(translationMessage); if (translationMessage.isTransient()) { translationMessage = translationMessageDao.insert(translationMessage); } else { translationMessage = translationMessageDao.update(translationMessage); } clearCacheOnCommit(); return translationMessage; } @Override public List<TranslationMessage> search(final TranslationMessageQuery query) { return translationMessageDao.search(query); } public void setCacheManager(final CacheManager cacheManager) { this.cacheManager = cacheManager; } public void setFetchServiceLocal(final FetchServiceLocal fetchService) { this.fetchService = fetchService; } public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) { this.settingsService = settingsService; } public void setTransactionHelper(final TransactionHelper transactionHelper) { this.transactionHelper = transactionHelper; } public void setTranslationMessageDao(final TranslationMessageDAO translationMessageDao) { this.translationMessageDao = translationMessageDao; } @Override public void validate(final TranslationMessage translationMessage) { getValidator().validate(translationMessage); } private void clearCacheOnCommit() { CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() { @Override public void onTransactionCommit() { getCache().clear(); } }); } private Cache getCache() { return cacheManager.getCache("cyclos.TranslationMessages"); } private Validator getValidator() { final Validator validator = new Validator(); validator.property("key").required().maxLength(100); validator.property("value").maxLength(4000); return validator; } private void importNewAndModifiedProperties(final Properties properties, final boolean emptyOnly) { // Process existing messages. This is done with Object[], otherwise hibernate will load each message with a separate select final Iterator<Object[]> existing = translationMessageDao.listData(); try { while (existing.hasNext()) { final Object[] data = existing.next(); final String key = (String) data[1]; final String currentValue = (String) data[2]; final String newValue = properties.getProperty(key); if (newValue != null) { final boolean shallUpdate = !newValue.equals(currentValue) && (!emptyOnly || StringUtils.isEmpty(currentValue)); if (shallUpdate) { final TranslationMessage message = new TranslationMessage(); message.setId((Long) data[0]); message.setKey(key); message.setValue(newValue); translationMessageDao.update(message, false); } properties.remove(key); } } } finally { DataIteratorHelper.close(existing); } fetchService.clearCache(); // Only those who have to be inserted are left in properties insertAll(properties); } private void importOnlyNewProperties(final Properties properties) { final Iterator<String> allKeys = translationMessageDao.listAllKeys(); try { while (allKeys.hasNext()) { final String key = allKeys.next(); properties.remove(key); } } finally { DataIteratorHelper.close(allKeys); } // Only new keys are left on the properties object insertAll(properties); } private void insertAll(final Properties properties) { final CacheCleaner cacheCleaner = new CacheCleaner(fetchService); for (final Map.Entry<Object, Object> entry : properties.entrySet()) { final String key = (String) entry.getKey(); final String value = (String) entry.getValue(); final TranslationMessage translationMessage = new TranslationMessage(); translationMessage.setKey(key); translationMessage.setValue(value); try { // Try to load first final TranslationMessage existing = translationMessageDao.load(key); // Existing - update existing.setValue(value); translationMessageDao.update(existing, false); } catch (final EntityNotFoundException e) { // Not found - insert translationMessageDao.insert(translationMessage); } // Clear the entity cache to avoid an explosion of messages in cache cacheCleaner.clearCache(); } } private void notifyListeners(final Properties properties) { for (TranslationChangeListener listener : listeners) { listener.onTranslationsChanged(properties); } } }