/*******************************************************************************
* Copyright (c) 2015 IBH SYSTEMS GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBH SYSTEMS GmbH - initial API and implementation
*******************************************************************************/
package org.eclipse.packagedrone.repo.aspect.upgrade;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.packagedrone.repo.ChannelAspectInformation;
import org.eclipse.packagedrone.repo.Version;
import org.eclipse.packagedrone.repo.aspect.ChannelAspectFactory;
import org.eclipse.packagedrone.repo.aspect.ChannelAspectProcessor;
import org.eclipse.packagedrone.repo.channel.ChannelInformation;
import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.manage.todo.BasicTask;
import org.eclipse.packagedrone.repo.manage.todo.DefaultTaskProvider;
import org.eclipse.packagedrone.repo.manage.todo.Task;
import org.eclipse.packagedrone.utils.profiler.Profile;
import org.eclipse.packagedrone.utils.profiler.Profile.Handle;
import org.eclipse.packagedrone.web.LinkTarget;
import org.eclipse.packagedrone.web.RequestMethod;
import org.eclipse.packagedrone.web.common.Button;
import org.eclipse.packagedrone.web.common.Modifier;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.net.UrlEscapers;
public class UpgradeTaskProvider extends DefaultTaskProvider implements EventHandler
{
private final static Logger logger = LoggerFactory.getLogger ( UpgradeTaskProvider.class );
private static final Button PERFORM_BUTTON = new Button ( "Refresh aspect", "refresh", Modifier.DEFAULT );
private static final Button PERFORM_ALL_BUTTON = new Button ( "Refresh channel", "refresh", Modifier.DEFAULT );
private static final Button PERFORM_ALL_SUPER_BUTTON = new Button ( "Refresh all channels", "refresh", Modifier.DEFAULT );
private final ServiceListener listener = new ServiceListener () {
@Override
public void serviceChanged ( final ServiceEvent event )
{
handleServiceChange ( event );
}
};
private ChannelService service;
private BundleContext context;
public UpgradeTaskProvider ()
{
}
public void setService ( final ChannelService service )
{
this.service = service;
}
public void start () throws Exception
{
logger.info ( "Starting channel aspect upgrade watcher" );
this.context = FrameworkUtil.getBundle ( UpgradeTaskProvider.class ).getBundleContext ();
this.context.addServiceListener ( this.listener, String.format ( "(%s=%s)", Constants.OBJECTCLASS, ChannelAspectFactory.class.getName () ) );
// fork off the initial scan, since the component has to start even if the database is not present
final Thread t = new Thread () {
@Override
public void run ()
{
refresh ();
}
};
t.setName ( "UpgradeTaskProvider/initialScan" );
t.setDaemon ( true );
t.start ();
}
public void stop ()
{
this.context.removeServiceListener ( this.listener );
}
protected void handleServiceChange ( final ServiceEvent event )
{
logger.debug ( "service change - {} - {}", event.getType (), event.getServiceReference () );
switch ( event.getType () )
{
case ServiceEvent.UNREGISTERING:
case ServiceEvent.REGISTERED:
refresh ();
break;
}
}
public void refresh ()
{
logger.info ( "Refreshing" );
setTasks ( updateState () );
}
private List<Task> updateState ()
{
try ( Handle handle = Profile.start ( this, "updateState" ) )
{
final Map<String, ChannelAspectInformation> infos = ChannelAspectProcessor.scanAspectInformations ( this.context );
final List<Task> result = new LinkedList<> ();
final Multimap<String, ChannelInformation> missing = HashMultimap.create ();
final Multimap<ChannelInformation, String> channels = HashMultimap.create ();
for ( final ChannelInformation channel : this.service.list () )
{
logger.debug ( "Checking channel: {}", channel.getId () );
final Map<String, String> states = channel.getAspectStates ();
for ( final Map.Entry<String, String> entry : states.entrySet () )
{
logger.debug ( "\t{}", entry.getKey () );
final ChannelAspectInformation info = infos.get ( entry.getKey () );
if ( info == null )
{
missing.put ( entry.getKey (), channel );
}
else
{
logger.debug ( "\t{} - {} -> {}", info.getFactoryId (), entry.getValue (), info.getVersion () );
if ( !info.getVersion ().equals ( Version.valueOf ( entry.getValue () ) ) )
{
result.add ( makeUpgradeTask ( channel, info, entry.getValue () ) );
channels.put ( channel, entry.getKey () );
}
}
}
}
for ( final Map.Entry<ChannelInformation, Collection<String>> entry : channels.asMap ().entrySet () )
{
final ChannelInformation channel = entry.getKey ();
final LinkTarget target = new LinkTarget ( String.format ( "/channel/%s/refreshAllAspects", UrlEscapers.urlPathSegmentEscaper ().escape ( channel.getId () ) ) );
final String description = "Channel aspects active in this channel have been updated. You can refresh the whole channel.";
result.add ( new BasicTask ( "Refresh channel: " + makeChannelTitle ( channel ), 100, description, target, RequestMethod.GET, PERFORM_ALL_BUTTON ) );
}
for ( final Map.Entry<String, Collection<ChannelInformation>> entry : missing.asMap ().entrySet () )
{
final String missingChannels = entry.getValue ().stream ().map ( ChannelInformation::getId ).collect ( Collectors.joining ( ", " ) );
result.add ( new BasicTask ( String.format ( "Fix missing channel aspect: %s", entry.getKey () ), 1, String.format ( "The channel aspect '%s' is being used but not installed in the system. Channels: %s", entry.getKey (), missingChannels ), null ) );
}
if ( !channels.isEmpty () )
{
result.add ( new BasicTask ( "Refresh all channels", 1, "Refresh all channels in one big task", new LinkTarget ( String.format ( "/job/%s/create", UpgradeAllChannelsJob.ID ) ), RequestMethod.POST, PERFORM_ALL_SUPER_BUTTON ) );
}
return result;
}
}
private String makeChannelTitle ( final ChannelInformation channel )
{
if ( channel.getName () != null )
{
return String.format ( "%s (%s)", channel.getName (), channel.getId () );
}
else
{
return channel.getId ();
}
}
private Task makeUpgradeTask ( final ChannelInformation channel, final ChannelAspectInformation info, final String fromVersion )
{
final String channelName = makeChannelTitle ( channel );
String factoryId;
try
{
factoryId = URLEncoder.encode ( info.getFactoryId (), "UTF-8" );
}
catch ( final UnsupportedEncodingException e )
{
factoryId = info.getFactoryId ();
}
final LinkTarget target = new LinkTarget ( String.format ( "/channel/%s/refreshAspect?aspect=%s", channel.getId (), factoryId ) );
final String description = String.format ( "The aspect %s (%s) in channel %s was upgraded from version %s to %s. The channel aspect has to be re-processed.", info.getLabel (), info.getFactoryId (), channelName, fromVersion, info.getVersion () );
return new BasicTask ( "Upgrade aspect data", 1_000, description, target, RequestMethod.POST, PERFORM_BUTTON );
}
@Override
public void handleEvent ( final Event event )
{
logger.debug ( "Received event - {}", event.getTopic () );
final String topic = event.getTopic ();
final Object op = event.getProperty ( "operation" );
if ( topic.startsWith ( "drone/channel/" ) )
{
if ( "remove".equals ( op ) || "refresh".equals ( op ) )
{
refresh ();
}
}
}
}