/* 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.application; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; import java.util.Set; import nl.strohalm.cyclos.dao.ApplicationDAO; import nl.strohalm.cyclos.dao.IndexOperationDAO; import nl.strohalm.cyclos.entities.Application; import nl.strohalm.cyclos.entities.Application.PasswordHash; import nl.strohalm.cyclos.entities.IndexOperation.EntityType; import nl.strohalm.cyclos.entities.IndexStatus; import nl.strohalm.cyclos.entities.Indexable; import nl.strohalm.cyclos.entities.access.SessionQuery; import nl.strohalm.cyclos.entities.accounts.LockedAccountsOnPayments; import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner; import nl.strohalm.cyclos.entities.accounts.transactions.Invoice; import nl.strohalm.cyclos.entities.accounts.transactions.InvoiceQuery; import nl.strohalm.cyclos.entities.alerts.Alert; import nl.strohalm.cyclos.entities.alerts.SystemAlert; import nl.strohalm.cyclos.entities.alerts.SystemAlert.Alerts; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.members.messages.MessageBox; import nl.strohalm.cyclos.entities.members.messages.MessageQuery; import nl.strohalm.cyclos.initializations.LocalInitialization; import nl.strohalm.cyclos.scheduling.PollingTasksHandler; import nl.strohalm.cyclos.scheduling.SchedulingHandler; import nl.strohalm.cyclos.scheduling.polling.PollingTask; import nl.strohalm.cyclos.services.InitializingService; import nl.strohalm.cyclos.services.access.AccessServiceLocal; import nl.strohalm.cyclos.services.accounts.rates.RateServiceLocal; import nl.strohalm.cyclos.services.alerts.AlertServiceLocal; import nl.strohalm.cyclos.services.alerts.ErrorLogServiceLocal; import nl.strohalm.cyclos.services.elements.MessageServiceLocal; import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal; import nl.strohalm.cyclos.services.transactions.InvoiceServiceLocal; import nl.strohalm.cyclos.utils.MessageResolver; import nl.strohalm.cyclos.utils.MessageResourcesLoadedListener; import nl.strohalm.cyclos.utils.TransactionHelper; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.cache.Cache; import nl.strohalm.cyclos.utils.cache.CacheCallback; import nl.strohalm.cyclos.utils.cache.CacheManager; import nl.strohalm.cyclos.utils.instance.InstanceHandler; import nl.strohalm.cyclos.utils.lucene.IndexHandler; import nl.strohalm.cyclos.utils.query.PageHelper; import nl.strohalm.cyclos.utils.tasks.TaskRunner; import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData; import nl.strohalm.cyclos.utils.transaction.TransactionCommitListener; import nl.strohalm.cyclos.utils.validation.ValidationException; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DateUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; /** * Implementation class for the application service interface. * @author rafael */ public class ApplicationServiceImpl implements ApplicationServiceLocal, ApplicationContextAware, InitializingBean, MessageResourcesLoadedListener { private static final Log LOG = LogFactory.getLog(ApplicationServiceImpl.class); private static final String APPLICATION_CACHE_KEY = "application"; private static final String MINOR_VERSION_TAG = "_minor_"; private static final String minorVersion; private static String extract(final String cvsTagName) { if (cvsTagName == null) { return ""; } String result = cvsTagName; result = result.replaceAll("\\$Name:", ""); result = result.replaceAll("\\$", ""); result = result.replaceAll(" ", ""); if (StringUtils.isBlank(result) || !result.contains(MINOR_VERSION_TAG)) { return ""; } else { String[] tmp = StringUtils.splitByWholeSeparator(result, MINOR_VERSION_TAG); return tmp[tmp.length - 1]; } } private MessageResolver messageResolver; static { // the parameter is interpreted by CVS and expanded minorVersion = extract("$Name: not supported by cvs2svn $"); } private boolean initialized; private boolean initializing; private boolean runScheduling; private ApplicationContext applicationContext; private AccessServiceLocal accessService; private AlertServiceLocal alertService; private MessageServiceLocal messageService; private InvoiceServiceLocal invoiceService; private ErrorLogServiceLocal errorLogService; private RateServiceLocal rateService; private ApplicationDAO applicationDao; private IndexOperationDAO indexOperationDao; private SchedulingHandler schedulingHandler; private PollingTasksHandler pollingTasksHandler; private InstanceHandler instanceHandler; private IndexHandler indexHandler; private CacheManager cacheManager; private TaskRunner taskRunner; private long startupTime; private Properties cyclosProperties; private TransactionHelper transactionHelper; private LockedAccountsOnPayments lockedAccountsOnPayments; private final HashMap<Alerts, String> deferredEvents = new HashMap<Alerts, String>(); private PermissionServiceLocal permissionService; @Override public void afterPropertiesSet() throws Exception { try { lockedAccountsOnPayments = LockedAccountsOnPayments.valueOf(cyclosProperties.getProperty("cyclos.lockedAccountsOnPayments", LockedAccountsOnPayments.ORIGIN.name()).toUpperCase()); } catch (Exception e) { StringBuilder message = new StringBuilder(); message.append("Invalid value for cyclos.lockedAccountsOnPayments: ").append(cyclosProperties.getProperty("cyclos.lockedAccountsOnPayments")).append(". Valid values are "); boolean first = true; for (LockedAccountsOnPayments item : LockedAccountsOnPayments.values()) { if (first) { first = false; } else { message.append(", "); } message.append(item.name().toLowerCase()); } throw new IllegalArgumentException(message.toString()); } } @Override public void awakePollingTask(final Class<? extends PollingTask> type) { if (pollingTasksHandler != null) { PollingTask pollingTask = pollingTasksHandler.get(type); if (pollingTask != null) { pollingTask.awake(); } } } @Override public void awakePollingTaskOnTransactionCommit(final Class<? extends PollingTask> type) { CurrentTransactionData.addTransactionCommitListener(new TransactionCommitListener() { @Override public void onTransactionCommit() { awakePollingTask(type); } }); } @Override public Calendar getAccountStatusEnabledSince() { return getApplication().getAccountStatusEnabledSince(); } @Override public ApplicationStatusVO getApplicationStatus() { final ApplicationStatusVO vo = new ApplicationStatusVO(); // Uptime period final long diff = System.currentTimeMillis() - startupTime; final int days = (int) (diff / DateUtils.MILLIS_PER_DAY); final int hours = (int) ((diff % DateUtils.MILLIS_PER_DAY) / DateUtils.MILLIS_PER_HOUR); vo.setUptimeDays(days); vo.setUptimeHours(hours); // Connected users SessionQuery sessions = new SessionQuery(); sessions.setGroups(permissionService.getAllVisibleGroups()); sessions.setPageForCount(); sessions.setNatures(Collections.singleton(Group.Nature.ADMIN)); vo.setConnectedAdmins(PageHelper.getTotalCount(accessService.searchSessions(sessions))); sessions.setNatures(Collections.singleton(Group.Nature.MEMBER)); vo.setConnectedMembers(PageHelper.getTotalCount(accessService.searchSessions(sessions))); sessions.setNatures(Collections.singleton(Group.Nature.BROKER)); vo.setConnectedBrokers(PageHelper.getTotalCount(accessService.searchSessions(sessions))); sessions.setNatures(Collections.singleton(Group.Nature.OPERATOR)); vo.setConnectedOperators(PageHelper.getTotalCount(accessService.searchSessions(sessions))); // Cyclos version vo.setCyclosVersion(getCyclosVersion()); // Number of alerts vo.setMemberAlerts(alertService.getAlertCount(Alert.Type.MEMBER)); vo.setSystemAlerts(alertService.getAlertCount(Alert.Type.SYSTEM)); vo.setErrors(errorLogService.getCount()); // Unread messages vo.setUnreadMessages(countUnreadMessages()); // Open invoices vo.setOpenInvoices(countOpenInvoices()); return vo; } @Override public String getCyclosVersion() { String suffix = ""; if (StringUtils.isNotBlank(minorVersion)) { suffix = " (" + minorVersion + ")"; } return getApplication().getVersion() + suffix; } @Override public Map<Class<? extends Indexable>, IndexStatus> getFullTextIndexesStatus() { final Map<Class<? extends Indexable>, IndexStatus> stats = new LinkedHashMap<Class<? extends Indexable>, IndexStatus>(); for (EntityType type : EntityType.values()) { Class<? extends Indexable> entityClass = type.getEntityClass(); stats.put(entityClass, indexHandler.getIndexStatus(entityClass)); } return stats; } @Override public LockedAccountsOnPayments getLockedAccountsOnPayments() { return lockedAccountsOnPayments; } public MessageResolver getMessageResolver() { return messageResolver; } @Override public PasswordHash getPasswordHash() { return getApplication().getPasswordHash(); } @Override public synchronized void initialize() { if (initialized || initializing) { return; } initializing = true; try { // Store the startup time startupTime = System.currentTimeMillis(); // Set AWT to headless mode. This way we can use XFree86 libs // on *nix systems without an X server running. setHeadlessMode(); // The InitializingServices should be only initialized when scheduling is used on this cyclos instance. runScheduling = !Boolean.valueOf(cyclosProperties.getProperty("cyclos.disableScheduling", "false")); if (runScheduling) { // Run the initializations (beans of type InitializingService) Set<String> beanNames = applicationContext.getBeansOfType(InitializingService.class).keySet(); taskRunner.runInitializations(beanNames); // Start both scheduling and polling tasks handlers schedulingHandler.start(); pollingTasksHandler.start(); } // Run the initializations Collection<LocalInitialization> initializations = applicationContext.getBeansOfType(LocalInitialization.class).values(); runAll(initializations); if (runScheduling) { // Create a system alert transactionHelper.runAsync(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { // at this point, the resource bundle for translations is not loaded yet // thus, we need to defer the creation of the event deferEvent(SystemAlert.Alerts.APPLICATION_RESTARTED, instanceHandler.getId()); } }); } initialized = true; // add this object as a listener to the messageResolver, which will inform (notify) this // object when the translation resource bundles have been loaded, so that finally alerts can be created messageResolver.addMessageResourcesLoadedListener(this); } finally { initializing = false; } } @Override public boolean isInitialized() { return initialized && !initializing; } @Override public boolean isOnline() { Application app = getApplication(); return app != null && app.isOnline(); } @Override public boolean isRunScheduling() { return runScheduling; } /** * * @see nl.strohalm.cyclos.utils.MessageResourcesLoadedListener#onApplicationResourcesLoaded() */ @Override public void onApplicationResourcesLoaded() { Iterator<Alerts> it = deferredEvents.keySet().iterator(); while (it.hasNext()) { Alerts key = it.next(); String val = deferredEvents.get(key); alertService.create(key, val); } } @Override public void purgeIndexOperations(final Calendar time) { Calendar limit = (Calendar) time.clone(); limit.add(Calendar.HOUR_OF_DAY, -24); indexOperationDao.deleteBefore(limit); } @Override public void rebuildIndexes(final Class<? extends Indexable> entityType) { for (Class<? extends Indexable> entityClass : resolveIndexedClasses(entityType)) { indexHandler.rebuild(entityClass); } } public void setAccessServiceLocal(final AccessServiceLocal accessService) { this.accessService = accessService; } public void setAlertServiceLocal(final AlertServiceLocal alertService) { this.alertService = alertService; } @Override public void setApplicationContext(final ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public void setApplicationDao(final ApplicationDAO applicationDao) { this.applicationDao = applicationDao; } public void setCacheManager(final CacheManager cacheManager) { this.cacheManager = cacheManager; } public void setCyclosProperties(final Properties cyclosProperties) { this.cyclosProperties = cyclosProperties; } public void setErrorLogServiceLocal(final ErrorLogServiceLocal errorLogService) { this.errorLogService = errorLogService; } public void setIndexHandler(final IndexHandler indexHandler) { this.indexHandler = indexHandler; } public void setIndexOperationDao(final IndexOperationDAO indexOperationDao) { this.indexOperationDao = indexOperationDao; } public void setInstanceHandler(final InstanceHandler instanceHandler) { this.instanceHandler = instanceHandler; } public void setInvoiceServiceLocal(final InvoiceServiceLocal invoiceService) { this.invoiceService = invoiceService; } public void setMessageResolver(final MessageResolver messageResolver) { this.messageResolver = messageResolver; } public void setMessageServiceLocal(final MessageServiceLocal messageService) { this.messageService = messageService; } @Override public void setOnline(final boolean online) { Application application = getApplication(); final boolean changed = application.isOnline() != online; if (changed) { if (online && (rateService.checkPendingRateInitializations(null) != null)) { throw new ValidationException("rates.error.notOnlineWhileRateInitsPending"); } application.setOnline(online); applicationDao.update(application); getCache().remove(APPLICATION_CACHE_KEY); if (!online) { // Disconnect all logged users but the current user accessService.disconnectAllButLogged(); } } } public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) { this.permissionService = permissionService; } public void setPollingTasksHandler(final PollingTasksHandler pollingTasksHandler) { this.pollingTasksHandler = pollingTasksHandler; } public void setRateServiceLocal(final RateServiceLocal rateService) { this.rateService = rateService; } public void setSchedulingHandler(final SchedulingHandler schedulingHandler) { this.schedulingHandler = schedulingHandler; } public void setTaskRunner(final TaskRunner taskRunner) { this.taskRunner = taskRunner; } public void setTransactionHelper(final TransactionHelper transactionHelper) { this.transactionHelper = transactionHelper; } @Override public void shutdown() { initialized = false; transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { alertService.create(SystemAlert.Alerts.APPLICATION_SHUTDOWN, instanceHandler.getId()); } }); applicationDao.shutdownDBIfNeeded(); } private int countOpenInvoices() { final InvoiceQuery query = new InvoiceQuery(); query.setOwner(SystemAccountOwner.instance()); query.setDirection(InvoiceQuery.Direction.INCOMING); query.setStatus(Invoice.Status.OPEN); query.setPageForCount(); return PageHelper.getTotalCount(invoiceService.search(query)); } private int countUnreadMessages() { final MessageQuery query = new MessageQuery(); query.setGetter(LoggedUser.element()); query.setMessageBox(MessageBox.INBOX); query.setRead(false); query.setPageForCount(); return PageHelper.getTotalCount(messageService.search(query)); } /** * Defer the event creation for later firing * * @param applicationRestarted Alert type * @param id application id */ private void deferEvent(final Alerts applicationRestarted, final String id) { deferredEvents.put(applicationRestarted, id); } private Application getApplication() { return getCache().get(APPLICATION_CACHE_KEY, new CacheCallback() { @Override public Object retrieve() { return applicationDao.read(); } }); } private Cache getCache() { return cacheManager.getCache("cyclos.Application"); } /** * Returns all indexed classes if the parameter is null, or a singleton collection otherwise */ private Collection<Class<? extends Indexable>> resolveIndexedClasses(final Class<? extends Indexable> entityType) { if (entityType == null) { Collection<Class<? extends Indexable>> entityClasses = new ArrayList<Class<? extends Indexable>>(); for (EntityType type : EntityType.values()) { entityClasses.add(type.getEntityClass()); } return entityClasses; } else { return Collections.<Class<? extends Indexable>> singleton(entityType); } } /** * Run all given local initializations in its own transaction */ private void runAll(final Collection<LocalInitialization> initializations) { for (final LocalInitialization initialization : initializations) { LOG.debug(String.format("Running initialization (%s)...", initialization.getName())); transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { initialization.initialize(); } }); } } private void setHeadlessMode() { System.setProperty("java.awt.headless", "true"); } }