/* * Copyright (C) 2003-2014 eXo Platform SAS. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.exoplatform.platform.upgrade.plugins; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.ValueFormatException; import javax.jcr.query.Query; import javax.jcr.query.QueryManager; import org.apache.commons.lang.StringUtils; import org.exoplatform.commons.api.notification.channel.AbstractChannel; import org.exoplatform.commons.api.notification.channel.ChannelManager; import org.exoplatform.commons.api.notification.service.setting.PluginContainer; import org.exoplatform.commons.api.settings.data.Scope; import org.exoplatform.commons.chromattic.ChromatticManager; import org.exoplatform.commons.notification.NotificationConfiguration; import org.exoplatform.commons.notification.NotificationUtils; import org.exoplatform.commons.notification.channel.MailChannel; import org.exoplatform.commons.notification.impl.AbstractService; import org.exoplatform.commons.notification.impl.NotificationSessionManager; import org.exoplatform.commons.notification.impl.setting.UserSettingServiceImpl; import org.exoplatform.commons.upgrade.UpgradeProductPlugin; import org.exoplatform.commons.utils.CommonsUtils; import org.exoplatform.commons.version.util.VersionComparator; import org.exoplatform.container.xml.InitParams; import org.exoplatform.services.jcr.ext.common.SessionProvider; import org.exoplatform.services.jcr.impl.core.query.QueryImpl; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.social.common.lifecycle.SocialChromatticLifeCycle; import org.exoplatform.social.core.chromattic.entity.IdentityEntity; /** * Created by The eXo Platform SAS * Author : eXoPlatform * exo@exoplatform.com * Dec 23, 2014 */ public class UpgradeUserNotificationSettingPlugin extends UpgradeProductPlugin { /** */ private static final Log LOG = ExoLogger.getLogger(UpgradeUserNotificationSettingPlugin.class); /** */ private static final int LIMIT_LOAD_THRESHOLD = 500; /** */ private static final String SETTING_LIFE_CYCLE_NAME = "setting"; /** */ private final String workspace; /** */ private final String settingWorkspace; /** */ private final String socialWorkspace; /** */ private String activeChannelList = null; /** */ private String inactiveChannelList = null; /** */ private Map<String, String> channelProperties; public UpgradeUserNotificationSettingPlugin(InitParams initParams, ChromatticManager manager) { super(initParams); NotificationConfiguration configuration = CommonsUtils.getService(NotificationConfiguration.class); this.workspace = configuration != null ? configuration.getWorkspace() : null; this.socialWorkspace = manager.getLifeCycle(SocialChromatticLifeCycle.SOCIAL_LIFECYCLE_NAME).getWorkspaceName(); this.settingWorkspace = manager.getLifeCycle(SETTING_LIFE_CYCLE_NAME).getWorkspaceName(); this.channelProperties = new HashMap<String, String>(); } @Override public void processUpgrade(String oldVersion, String newVersion) { if (workspace == null) { return; } boolean created = NotificationSessionManager.createSystemProvider(); SessionProvider sProvider = NotificationSessionManager.getSessionProvider(); loadChannels(); int offset = 0; long total = System.currentTimeMillis(); try { Session socialSession = getJCRSession(sProvider, socialWorkspace); NodeIterator ni = getIdentityNodes(socialSession, offset, LIMIT_LOAD_THRESHOLD); Node node = null; String remoteId = null; long t = 0; while (ni.hasNext()) { node = ni.nextNode(); remoteId = node.getProperty(IdentityEntity.remoteId.getName()).getString(); t = System.currentTimeMillis(); LOG.info(String.format("| \\ START::user number: %s (%s user)", offset, remoteId)); processUpgrade(sProvider, remoteId); offset++; LOG.info(String.format("| // END::Migration setting (%s user) consumed time: %s", remoteId, System.currentTimeMillis() - t)); if (offset % LIMIT_LOAD_THRESHOLD == 0) { socialSession.logout(); socialSession = null; socialSession = getJCRSession(sProvider, socialWorkspace); ni = getIdentityNodes(socialSession, offset, LIMIT_LOAD_THRESHOLD); } } } catch (Exception e) { LOG.error(e.getMessage(), e); } finally { LOG.info(String.format("| DONE::total: %s user(s) consumed time: %s", offset, System.currentTimeMillis() - total)); NotificationSessionManager.closeSessionProvider(created); } } private void processUpgrade(SessionProvider sProvider, String remoteId) throws PathNotFoundException, RepositoryException { Node userNode = getUserSettingNode(sProvider, remoteId); // if (userNode == null) { addMixin(sProvider, remoteId); return; } boolean mustMigration = mustUpgrade(userNode); // String globalPath = Scope.GLOBAL.toString().toLowerCase(); // Node userSettingNode = userNode.hasNode(globalPath) ? userNode.getNode(globalPath) : null; //do normalization the user setting UpgradeBuilder upgradeBuilder = builder(userSettingNode).doNormalization(userNode, remoteId); // if (userSettingNode != null && mustMigration) { upgradeBuilder.upgradeDaily() .upgradeWeekly() .upgradeInstantly() .upgradeIsActive() .upgradeChannels(); } // upgradeBuilder.done(); } /** * Creates the new upgrade builder with userSetting node * * @param userSettingNode * @return the new instance of builder */ private UpgradeBuilder builder(Node userSettingNode) { return new UpgradeBuilder(userSettingNode); } /** * Upgrade setting builder * * @author thanhvc * */ private class UpgradeBuilder { private final Node userSettingNode; private UpgradeBuilder(Node userSettingNode) { this.userSettingNode = userSettingNode; } private UpgradeBuilder upgradeDaily() throws RepositoryException { upgradeProperty(userSettingNode, AbstractService.EXO_DAILY, null); return this; } private UpgradeBuilder upgradeWeekly() throws RepositoryException { upgradeProperty(userSettingNode, AbstractService.EXO_WEEKLY, null); return this; } private UpgradeBuilder upgradeInstantly() throws RepositoryException { upgradeProperty(userSettingNode, AbstractService.EXO_INSTANTLY, null); return this; } private UpgradeBuilder upgradeIsActive() throws RepositoryException { upgradeProperty(userSettingNode, AbstractService.EXO_IS_ACTIVE, null); return this; } private UpgradeBuilder upgradeChannels() throws RepositoryException { LOG.info(String.format(" %s channel(s) will be add the plugins in the user setting", channelProperties.size())); for (Map.Entry<String , String> entry : channelProperties.entrySet()) { String key = getChannelProperty(entry.getKey()); upgradeProperty(userSettingNode, key, entry.getValue()); } return this; } private UpgradeBuilder doNormalization(Node userNode, String remoteId) throws RepositoryException { normalize(userNode, remoteId); if (userSettingNode == null) { userNode.getSession().save(); } return this; } private void done() throws RepositoryException { if (userSettingNode != null) { userSettingNode.getSession().save(); } } } /** * Gets the channel key * @param channelId the channel Id * @return the channel key */ private String getChannelProperty(String channelId) { return UserSettingServiceImpl.NAME_PATTERN.replace("{CHANNELID}", channelId); } /** * Gets the channels what is using for Active setting. * * @return */ private String loadActiveValue() { if (this.activeChannelList == null) { ChannelManager channelManager = CommonsUtils.getService(ChannelManager.class); List<String> channelIds = new ArrayList<String> (); for (AbstractChannel channel : channelManager.getChannels()) { channelIds.add(channel.getId()); } activeChannelList = StringUtils.join(channelIds, ','); } return activeChannelList; } /** * Loads the channel list with default active plugins except Mail channel */ private void loadChannels() { if (this.channelProperties == null || channelProperties.size() == 0) { PluginContainer container = CommonsUtils.getService(PluginContainer.class); ChannelManager channelManager = CommonsUtils.getService(ChannelManager.class); for (AbstractChannel channel : channelManager.getChannels()) { if (!MailChannel.ID.equals(channel.getId())) { channelProperties.put(channel.getId(), NotificationUtils.listToString(container.getDefaultActivePlugins(), AbstractService.VALUE_PATTERN)); } } } } /** * If user setting is inactive (Never Notify Me). Mail Channel is disable, others is enable * * @return */ private String loadInactiveValue() { if (this.inactiveChannelList == null) { ChannelManager channelManager = CommonsUtils.getService(ChannelManager.class); List<String> channelIds = new ArrayList<String> (); for (AbstractChannel channel : channelManager.getChannels()) { if (!MailChannel.ID.equals(channel.getId())) { channelIds.add(channel.getId()); } } inactiveChannelList = StringUtils.join(channelIds, ','); } return inactiveChannelList; } @Override public boolean shouldProceedToUpgrade(String newVersion, String previousVersion) { return VersionComparator.isAfter(newVersion,previousVersion); } /** * The target of method upgrades the old setting from 4.1 to new notification setting in 4.2 * * Case 1: EXO_DAILY and EXO_WEEKLY changes to {<PluginA>}, {<PluginB>}... * Case 2: EXO_INSTANTLY changes to EXO_EMAILCHANNEL = {<PluginA>}, {<PluginB>} * Case 3: EXO_IS_ACTIVE = TRUE/FALSE: * TRUE > Enable ALL CHANNELS, FALSE: Only MAIL Disable * * @param userSettingNode * @param property * @param propertyValue * @throws RepositoryException * @throws PathNotFoundException * @throws ValueFormatException * @throws Exception */ private void upgradeProperty(Node userSettingNode, String property, String propertyValue) throws ValueFormatException, PathNotFoundException, RepositoryException { if (AbstractService.EXO_DAILY.equals(property) && userSettingNode.hasProperty(AbstractService.EXO_DAILY)) { Value value = userSettingNode.getProperty(property).getValue(); /** Case 1: EXO_DAILY changes to {<PluginA>}, {<PluginB>}...*/ if (value != null) { String oldValue = value.getString(); //exo:daily:'LikePlugin,SpaceInvitationPlugin' in PLF 4.1 //exo:daily:'{LikePlugin},{SpaceInvitationPlugin}' in PLF 4.2 //NotificationUtils.listToString uses to transform that String newValue = NotificationUtils.listToString(NotificationUtils.stringToList(oldValue), AbstractService.VALUE_PATTERN); userSettingNode.setProperty(property, newValue); } } else if (AbstractService.EXO_WEEKLY.equals(property) && userSettingNode.hasProperty(AbstractService.EXO_WEEKLY)) { Value value = userSettingNode.getProperty(property).getValue(); /** Case 1: EXO_WEEKLY changes to {<PluginA>}, {<PluginB>}...*/ if (value != null) { String oldValue = value.getString(); //exo:weekly:'LikePlugin,SpaceInvitationPlugin' in PLF 4.1 //exo:weekly:'{LikePlugin},{SpaceInvitationPlugin}' in PLF 4.2 //NotificationUtils.listToString uses to transform that String newValue = NotificationUtils.listToString(NotificationUtils.stringToList(oldValue), AbstractService.VALUE_PATTERN); userSettingNode.setProperty(property, newValue); } } else if (AbstractService.EXO_INSTANTLY.equals(property)) { if (userSettingNode.hasProperty(AbstractService.EXO_INSTANTLY)) { Value value = userSettingNode.getProperty(property).getValue(); /**Case 2: EXO_INSTANTLY changes to exo:MAIL_CHANNELChannel = {<PluginA>}, {<PluginB>}*/ if (value != null) { String oldValue = value.getString(); String newValue = NotificationUtils.listToString(NotificationUtils.stringToList(oldValue), AbstractService.VALUE_PATTERN); userSettingNode.setProperty(UserSettingServiceImpl.NAME_PATTERN.replace("{CHANNELID}", MailChannel.ID), newValue); //remove exo:instantly property //Passing a null as the second parameter removes the property. //It is equivalent to calling remove on the Property object itself. //For example, N.setProperty("P", (Value)null) would remove property called "P" of the node in N. userSettingNode.setProperty(AbstractService.EXO_INSTANTLY, (Value)null); } } } else if (AbstractService.EXO_IS_ACTIVE.equals(property)) { if (userSettingNode.hasProperty(AbstractService.EXO_IS_ACTIVE)) { Value value = userSettingNode.getProperty(property).getValue(); if (value != null) { String oldValue = value.getString(); String newValue = ""; if ("true".equals(oldValue)) {//Setting not upgraded and mail is enabled newValue = loadActiveValue(); } else if ("false".equals(oldValue) || oldValue.isEmpty()) {//Setting not upgraded and mail is disabled newValue = loadInactiveValue(); } else {//Setting has already upgraded newValue = oldValue; } userSettingNode.setProperty(AbstractService.EXO_IS_ACTIVE, newValue); } } } else { userSettingNode.setProperty(property, propertyValue); } } /** * Loads the identities with offset and limit * * @param session * @param offset * @param limit * @return the identity list */ private NodeIterator getIdentityNodes(Session session, int offset, int limit) { // try { StringBuilder sqlQuery = new StringBuilder("SELECT * FROM ").append("soc:identitydefinition") .append(" WHERE ") .append(" (") .append("jcr:path LIKE '") .append("/production/soc:providers/soc:organization/%'") .append(" AND NOT jcr:path LIKE '") .append("/production/soc:providers/soc:organization/%/%'") .append(")"); String queryStatement = sqlQuery.toString(); QueryManager queryMgr = session.getWorkspace().getQueryManager(); Query query = queryMgr.createQuery(queryStatement, Query.SQL); QueryImpl impl = (QueryImpl) query; // impl.setOffset(offset); impl.setLimit(limit); // return query.execute().getNodes(); } catch (Exception ex) { LOG.error("Query is failed!.", ex); return null; } } private Session getJCRSession(SessionProvider sProvider, String wpName) { Session session = null; try { session = sProvider.getSession(wpName, CommonsUtils.getRepository()); } catch (RepositoryException e) { LOG.error(e); } return session; } /** * Adds the default setting(mixintype) to the User setting * * @param sProvider * @param userName the give userName */ private void addMixin(SessionProvider sProvider, String userName) { try { Session session = getJCRSession(sProvider, settingWorkspace); Node userHomeNode = getUserSettingHome(session); Node userNode = userHomeNode.addNode(userName, AbstractService.STG_SIMPLE_CONTEXT); if (userNode.canAddMixin(AbstractService.MIX_DEFAULT_SETTING)) { userNode.addMixin(AbstractService.MIX_DEFAULT_SETTING); LOG.debug("|| Done to addMixin default setting for user: " + userName); } session.save(); } catch (Exception e) { LOG.error("Failed to addMixin for user notification setting", e); } } private Node getUserSettingHome(Session session) throws Exception { Node settingNode = session.getRootNode().getNode(AbstractService.SETTING_NODE); Node userHomeNode = null; if (settingNode.hasNode(AbstractService.SETTING_USER_NODE) == false) { userHomeNode = settingNode.addNode(AbstractService.SETTING_USER_NODE, AbstractService.STG_SUBCONTEXT); session.save(); } else { userHomeNode = settingNode.getNode(AbstractService.SETTING_USER_NODE); } return userHomeNode; } /** * Gets the user setting node by the give userName. * * @param sProvider * @param userName * @return Setting node or NULL if not found */ private Node getUserSettingNode(SessionProvider sProvider, String userName) { Session session = getJCRSession(sProvider, settingWorkspace); try { return (Node) session.getItem("/" + AbstractService.SETTING_USER_PATH + "/" + userName); } catch (Exception e) { return null; } } /** * Must upgrade only happens when the user setting has already the available notification setting. * * Notice: * In the case, user setting has both mixin and notif setting, it will be normalize setting by normalize() method * * @param userNode * @return */ private boolean mustUpgrade(Node userNode) { try { //user has both defaul setting and user setting if (userNode.isNodeType(AbstractService.MIX_DEFAULT_SETTING)) { if (userNode.hasNode(Scope.GLOBAL.toString().toLowerCase())) { Node global = userNode.getNode(Scope.GLOBAL.toString().toLowerCase()); return global.hasProperty(AbstractService.EXO_INSTANTLY) || global.hasProperty(AbstractService.EXO_DAILY) || global.hasProperty(AbstractService.EXO_WEEKLY); } else { return false; } } //case without default setting and have user setting if (!userNode.isNodeType(AbstractService.MIX_DEFAULT_SETTING)) { if (userNode.hasNode(Scope.GLOBAL.toString().toLowerCase())) { Node global = userNode.getNode(Scope.GLOBAL.toString().toLowerCase()); return global.hasProperty(AbstractService.EXO_INSTANTLY) || global.hasProperty(AbstractService.EXO_DAILY) || global.hasProperty(AbstractService.EXO_WEEKLY); } } return false; } catch (Exception e) { return false; } } /** * Normalize the user setting following the cases: * * CASE 1. * Mixin type + Global node * - exo:daily, exo:weekly, or exo:instantly is existing >> remove Mixin type. * * CASE 2 * No Mixin type + Global node * - WITHOUT exo:daily, exo:weekly, or exo:instantly is NOT existing>> Add mixin type * * CASE 3 * No Mixin type + WITHOUT Global node * - Add mixin type * * @param userNode * @param remoteId */ private void normalize(Node userNode, String remoteId) { try { //user with default setting if (userNode.isNodeType(AbstractService.MIX_DEFAULT_SETTING)) { if (userNode.hasNode(Scope.GLOBAL.toString().toLowerCase())) { Node global = userNode.getNode(Scope.GLOBAL.toString().toLowerCase()); if (global.hasProperty(AbstractService.EXO_INSTANTLY) || global.hasProperty(AbstractService.EXO_DAILY) || global.hasProperty(AbstractService.EXO_WEEKLY)) { LOG.info(String.format(" CASE 1:: %s user has both mixin and notif setting >> Action: remove mixin", remoteId)); userNode.removeMixin(AbstractService.MIX_DEFAULT_SETTING); return; } } } //user doesn't have default setting and without Global node if (!userNode.isNodeType(AbstractService.MIX_DEFAULT_SETTING)) { if (userNode.hasNode(Scope.GLOBAL.toString().toLowerCase())) { Node global = userNode.getNode(Scope.GLOBAL.toString().toLowerCase()); if (!global.hasProperty(AbstractService.EXO_INSTANTLY) && !global.hasProperty(AbstractService.EXO_DAILY) && !global.hasProperty(AbstractService.EXO_WEEKLY)) { LOG.info(String.format(" CASE 2:: %s user has NOT both mixin and notif setting >> Action: add mixin", remoteId)); userNode.addMixin(AbstractService.MIX_DEFAULT_SETTING); return; } } else { LOG.info(String.format(" CASE 3:: %s user has NOT both mixin and global node >> Action: add mixin", remoteId)); userNode.addMixin(AbstractService.MIX_DEFAULT_SETTING); return; } } } catch (Exception e) { LOG.error(e.getMessage(), e); } } }