UpgradeUserNotificationSettingPlugin.java
/*
* 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);
}
}
}