/******************************************************************************* * 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.channel.impl; import static org.eclipse.packagedrone.repo.utils.Splits.split; import static org.eclipse.packagedrone.utils.Locks.lock; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.aspect.ChannelAspectProcessor; import org.eclipse.packagedrone.repo.channel.AspectableChannel; import org.eclipse.packagedrone.repo.channel.ChannelDetails; import org.eclipse.packagedrone.repo.channel.ChannelId; import org.eclipse.packagedrone.repo.channel.ChannelInformation; import org.eclipse.packagedrone.repo.channel.ChannelNotFoundException; import org.eclipse.packagedrone.repo.channel.ChannelService; import org.eclipse.packagedrone.repo.channel.DeployKeysChannelAdapter; import org.eclipse.packagedrone.repo.channel.DescriptorAdapter; import org.eclipse.packagedrone.repo.channel.ModifiableChannel; import org.eclipse.packagedrone.repo.channel.ReadableChannel; import org.eclipse.packagedrone.repo.channel.deploy.DeployAuthService; import org.eclipse.packagedrone.repo.channel.deploy.DeployGroup; import org.eclipse.packagedrone.repo.channel.deploy.DeployKey; import org.eclipse.packagedrone.repo.channel.provider.AccessContext; import org.eclipse.packagedrone.repo.channel.provider.Channel; import org.eclipse.packagedrone.repo.channel.provider.ChannelProvider; import org.eclipse.packagedrone.repo.channel.provider.ChannelProvider.Listener; import org.eclipse.packagedrone.repo.channel.provider.ModifyContext; import org.eclipse.packagedrone.repo.channel.provider.ProviderInformation; import org.eclipse.packagedrone.repo.channel.stats.ChannelStatistics; import org.eclipse.packagedrone.storage.apm.StorageManager; import org.eclipse.packagedrone.storage.apm.StorageRegistration; import org.eclipse.packagedrone.utils.Locks.Locked; import org.eclipse.scada.utils.str.Tables; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; public class ChannelServiceImpl implements ChannelService, DeployAuthService { private class Entry implements Listener { private final ChannelProvider service; private final Map<String, Channel> channels = new HashMap<> (); public Entry ( final ChannelProvider service ) { this.service = service; this.service.addListener ( this ); addProvider ( service ); } public void dispose () { removeProvider ( this.service ); this.service.removeListener ( this ); handleUpdate ( this.service, null, this.channels.values () ); this.channels.clear (); } @Override public void update ( final Collection<? extends Channel> added, final Collection<? extends Channel> removed ) { if ( added != null ) { added.forEach ( channel -> this.channels.put ( channel.getId (), channel ) ); } if ( removed != null ) { removed.forEach ( channel -> this.channels.remove ( channel.getId () ) ); } handleUpdate ( this.service, added, removed ); } } private static final MetaKey KEY_STORAGE = new MetaKey ( "channels", "service" ); private final BundleContext context; private final ServiceTrackerCustomizer<ChannelProvider, Entry> customizer = new ServiceTrackerCustomizer<ChannelProvider, ChannelServiceImpl.Entry> () { @Override public Entry addingService ( final ServiceReference<ChannelProvider> reference ) { return new Entry ( ChannelServiceImpl.this.context.getService ( reference ) ); } @Override public void modifiedService ( final ServiceReference<ChannelProvider> reference, final Entry service ) { } @Override public void removedService ( final ServiceReference<ChannelProvider> reference, final Entry service ) { service.dispose (); } }; /** * Used to read-lock the channel and provider map */ private final Lock readLock; /** * Used to write-lock the channel and provider map */ private final Lock writeLock; private final ServiceTracker<ChannelProvider, Entry> tracker; private final Map<String, ChannelEntry> channelMap = new HashMap<> (); private final Map<String, ChannelProvider> providerMap = new HashMap<> (); private final Set<ProviderInformation> providers = new CopyOnWriteArraySet<> (); private final Set<ProviderInformation> unmodProviders = Collections.unmodifiableSet ( this.providers ); private StorageManager manager; private StorageRegistration handle; /** * Map channel ids to deploy groups, cache */ private final Multimap<String, DeployGroup> deployKeysMap = HashMultimap.create (); private ChannelAspectProcessor aspectProcessor; public ChannelServiceImpl () { this.context = FrameworkUtil.getBundle ( ChannelServiceImpl.class ).getBundleContext (); final ReentrantReadWriteLock lock = new ReentrantReadWriteLock (); this.readLock = lock.readLock (); this.writeLock = lock.writeLock (); this.tracker = new ServiceTracker<ChannelProvider, Entry> ( this.context, ChannelProvider.class, this.customizer ); } public void setStorageManager ( final StorageManager manager ) { this.manager = manager; } public void handleUpdate ( final ChannelProvider provider, final Collection<? extends Channel> added, final Collection<? extends Channel> removed ) { try ( final Locked l = lock ( this.writeLock ) ) { // process additions if ( added != null ) { added.forEach ( channel -> { final String mappedId = makeMappedId ( provider, channel ); final String name = mapName ( mappedId ); this.channelMap.put ( mappedId, new ChannelEntry ( new ChannelId ( mappedId, name ), channel, provider ) ); } ); } // process removals if ( removed != null ) { removed.forEach ( channel -> { final String mappedId = makeMappedId ( provider, channel ); this.channelMap.remove ( mappedId ); } ); } } } private String mapName ( final String mappedId ) { return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> model.mapToName ( mappedId ) ); } private static String makeMappedId ( final ChannelProvider provider, final Channel channel ) { return makeMappedId ( provider.getId (), channel.getId () ); } private static String makeMappedId ( final String providerId, final String channelId ) { return String.format ( "%s_%s", providerId, channelId ); } public void start () { this.aspectProcessor = new ChannelAspectProcessor ( this.context ); this.handle = this.manager.registerModel ( 1_000, KEY_STORAGE, new ChannelServiceModelProvider () ); this.manager.accessRun ( KEY_STORAGE, ChannelServiceAccess.class, ( model ) -> updateDeployGroupCache ( model ) ); try ( Locked l = lock ( this.writeLock ) ) { this.tracker.open (); } } public void stop () { try ( Locked l = lock ( this.writeLock ) ) { this.tracker.close (); } if ( this.handle != null ) { this.handle.unregister (); this.handle = null; } if ( this.aspectProcessor != null ) { this.aspectProcessor.close (); this.aspectProcessor = null; } } private static ChannelInformation accessState ( final ChannelEntry channelEntry ) { return accessRead ( channelEntry, channel -> channel.getInformation () ); } @Override public Collection<ChannelInformation> list () { try ( Locked l = lock ( this.readLock ) ) { return this.channelMap.values ().stream ().map ( ChannelServiceImpl::accessState ).collect ( Collectors.toList () ); } } /** * Find by the locator * <p> * This method does not acquire the read lock, this has to be done by the * caller * </p> * * @param by * the locator * @return the result */ protected Optional<ChannelEntry> find ( final By by ) { switch ( by.getType () ) { case ID: return Optional.ofNullable ( this.channelMap.get ( by.getQualifier () ) ); case NAME: return findByName ( (String)by.getQualifier () ); case COMPOSITE: { final By[] bys = (By[])by.getQualifier (); for ( final By oneBy : bys ) { final Optional<ChannelEntry> result = find ( oneBy ); if ( result.isPresent () ) { return result; } } return Optional.empty (); } default: throw new IllegalArgumentException ( String.format ( "Unknown locator type: %s", by.getType () ) ); } } /** * Find a channel by name * * @param name * the channel name to look for * @return the optional channel entry, never returns {@code null} but my * return {@link Optional#empty()}. */ private Optional<ChannelEntry> findByName ( final String name ) { if ( name == null ) { return Optional.empty (); } // FIXME: improve performance for ( final ChannelEntry entry : this.channelMap.values () ) { if ( name.equals ( entry.getId ().getName () ) ) { return Optional.of ( entry ); } } return Optional.empty (); } @Override public Optional<ChannelInformation> getState ( final By by ) { try ( Locked l = lock ( this.readLock ) ) { return find ( by ).map ( ChannelServiceImpl::accessState ); } } public void addProvider ( final ChannelProvider provider ) { try ( Locked l = lock ( this.writeLock ) ) { final ProviderInformation info = provider.getInformation (); this.providerMap.put ( info.getId (), provider ); this.providers.add ( info ); } } public void removeProvider ( final ChannelProvider provider ) { try ( Locked l = lock ( this.writeLock ) ) { final ProviderInformation info = provider.getInformation (); this.providerMap.remove ( info.getId () ); this.providers.remove ( info ); } } @Override public Collection<ProviderInformation> getProviders () { try ( Locked l = lock ( this.readLock ) ) { return this.unmodProviders; } } @Override public ChannelId create ( final String providerId, final ChannelDetails description ) { ChannelProvider provider; try ( Locked l = lock ( this.readLock ) ) { if ( providerId != null ) { provider = this.providerMap.get ( providerId ); } else if ( this.providerMap.size () == 1 ) { provider = this.providerMap.values ().iterator ().next (); } else { throw new IllegalArgumentException ( "No provider selected, but there is more than one provider available." ); } } final Channel channel = provider.create ( description, localId -> makeMappedId ( providerId, localId ) ); final String id = makeMappedId ( provider, channel ); return new ChannelId ( id, mapName ( id ) ); } @Override public boolean delete ( final By by ) { try ( Locked l = lock ( this.writeLock ) ) { final Optional<ChannelEntry> channel = find ( by ); if ( !channel.isPresent () ) { return false; } final ChannelEntry entry = channel.get (); // explicitly delete the mapping deleteChannel ( entry.getId ().getId () ); handleUpdate ( entry.getProvider (), null, Collections.singleton ( entry.getChannel () ) ); entry.getChannel ().delete (); return true; } } @SuppressWarnings ( "unchecked" ) @Override public <R, T> R accessCall ( final By by, final Class<T> clazz, final ChannelOperation<R, T> operation ) { if ( ReadableChannel.class.equals ( clazz ) ) { return accessRead ( findChannel ( by ), (ChannelOperation<R, ReadableChannel>)operation ); } else if ( ModifiableChannel.class.equals ( clazz ) ) { return accessModify ( findChannel ( by ), (ChannelOperation<R, ModifiableChannel>)operation ); } else if ( DeployKeysChannelAdapter.class.equals ( clazz ) ) { return handleDeployKeys ( findChannel ( by ), (ChannelOperation<R, DeployKeysChannelAdapter>)operation ); } else if ( DescriptorAdapter.class.equals ( clazz ) ) { return handleDescribe ( findChannel ( by ), (ChannelOperation<R, DescriptorAdapter>)operation ); } else if ( AspectableChannel.class.equals ( clazz ) ) { return accessModify ( findChannel ( by ), (ChannelOperation<R, ModifiableChannel>)operation ); } else { throw new IllegalArgumentException ( String.format ( "Unknown channel adapter: %s", clazz.getName () ) ); } } private static <R> R accessRead ( final ChannelEntry channelEntry, final ChannelOperation<R, ReadableChannel> operation ) { return channelEntry.getChannel ().accessCall ( ctx -> { try ( Disposing<AccessContext> wrappedCtx = Disposing.proxy ( AccessContext.class, ctx ); Disposing<ReadableChannel> channel = Disposing.proxy ( ReadableChannel.class, new ReadableChannelAdapter ( channelEntry.getId (), wrappedCtx.getTarget () ) ) ) { return operation.process ( channel.getTarget () ); } } , localId -> makeMappedId ( channelEntry.getProvider ().getId (), localId ) ); } private <T, R> R accessModify ( final ChannelEntry channelEntry, final ChannelOperation<R, ModifiableChannel> operation ) { return channelEntry.getChannel ().modifyCall ( ctx -> { try ( Disposing<ModifyContext> wrappedCtx = Disposing.proxy ( ModifyContext.class, ctx ); Disposing<ModifiableChannel> channel = Disposing.proxy ( ModifiableChannel.class, new ModifiableChannelAdapter ( channelEntry.getId (), wrappedCtx.getTarget (), this.aspectProcessor ) ) ) { return operation.process ( channel.getTarget () ); } } , localId -> makeMappedId ( channelEntry.getProvider ().getId (), localId ) ); } private <R> R handleDeployKeys ( final ChannelEntry channel, final ChannelOperation<R, DeployKeysChannelAdapter> operation ) { try ( Locked l = lock ( this.writeLock ) ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { final DeployKeysChannelAdapterImpl adapter = new DeployKeysChannelAdapterImpl ( channel.getId ().getId (), model) { @Override public void assignDeployGroup ( final String groupId ) { super.assignDeployGroup ( groupId ); StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( this.model ); } ); } @Override public void unassignDeployGroup ( final String groupId ) { super.unassignDeployGroup ( groupId ); StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( this.model ); } ); } }; return runDisposing ( operation, DeployKeysChannelAdapter.class, adapter ); } ); } } private <R, T> R runDisposing ( final ChannelOperation<R, T> operation, final Class<T> clazz, final T target ) { try ( Disposing<T> adapter = Disposing.proxy ( clazz, target ) ) { return operation.process ( adapter.getTarget () ); } catch ( final Exception e ) { throw new RuntimeException ( e ); } } @Override public Optional<Collection<DeployGroup>> getChannelDeployGroups ( final By by ) { try ( Locked l = lock ( this.readLock ) ) { final Optional<ChannelEntry> channelEntry = find ( by ); if ( !channelEntry.isPresent () ) { return Optional.empty (); } return Optional.ofNullable ( this.deployKeysMap.get ( channelEntry.get ().getId ().getId () ) ).map ( Collections::unmodifiableCollection ); } } private <R> R handleDescribe ( final ChannelEntry channelEntry, final ChannelOperation<R, DescriptorAdapter> operation ) { try ( Locked l = lock ( this.writeLock ) ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { final DescriptorAdapter dai = new DescriptorAdapterImpl ( channelEntry) { @Override public void setName ( final String name ) { model.putMapping ( getDescriptor ().getId (), name ); super.setName ( name ); final ChannelId desc = getDescriptor (); StorageManager.executeAfterPersist ( () -> { channelEntry.setId ( desc ); } ); } }; return runDisposing ( operation, DescriptorAdapter.class, dai ); } ); } } private ChannelEntry findChannel ( final By by ) { final Optional<ChannelEntry> channel; try ( Locked l = lock ( this.readLock ) ) { channel = find ( by ); } if ( !channel.isPresent () ) { throw new ChannelNotFoundException ( "fixme" ); } return channel.get (); } @Override public Map<String, String> getUnclaimedMappings () { try ( Locked l = lock ( this.readLock ) ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { final Map<String, String> map = model.getNameMap (); for ( final ChannelEntry entry : this.channelMap.values () ) { map.remove ( entry.getId () ); } return map; } ); } } /** * This is a console command */ public void listUnclaimedMappings () { final Map<String, String> map = getUnclaimedMappings (); final List<List<String>> rows = new ArrayList<> ( map.size () ); for ( final Map.Entry<String, String> entry : map.entrySet () ) { final ArrayList<String> row = new ArrayList<> ( 2 ); row.add ( entry.getKey () ); row.add ( entry.getValue () ); rows.add ( row ); } Tables.showTable ( System.out, Arrays.asList ( "ID", "Name" ), rows, 2 ); } public void deleteChannel ( final String channelId ) { try ( Locked l = lock ( this.writeLock ) ) { this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> { model.deleteChannel ( channelId ); StorageManager.executeAfterPersist ( () -> { // try to remove from mapped channels final ChannelEntry channel = this.channelMap.get ( channelId ); if ( channel != null ) { channel.setId ( new ChannelId ( channelId, null ) ); } // remove deploy groups for channel this.deployKeysMap.removeAll ( channelId ); } ); } ); } } @Override public void deleteMapping ( final String id, final String name ) { try ( final Locked l = lock ( this.writeLock ) ) { this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> internalDeleteMapping ( id, name, model ) ); } } private void internalDeleteMapping ( final String id, final String name, final ChannelServiceModify model ) { final String affectedId = model.deleteMapping ( id, name ); if ( affectedId != null ) { StorageManager.executeAfterPersist ( () -> { final ChannelEntry channel = this.channelMap.get ( id ); // try to remove from mapped channels if ( channel != null ) { channel.setId ( new ChannelId ( id, null ) ); } } ); } } /** * Update the channel to deploy group cache map * * @param model * the model to fill the cache from */ private void updateDeployGroupCache ( final ChannelServiceAccess model ) { // this will simply rebuild the complete map // clear first this.deployKeysMap.clear (); // fill afterwards for ( final Map.Entry<String, Set<String>> entry : model.getDeployGroupMap ().entrySet () ) { final String channelId = entry.getKey (); final List<DeployGroup> groups = entry.getValue ().stream ().map ( groupId -> model.getDeployGroup ( groupId ) ).collect ( Collectors.toList () ); this.deployKeysMap.putAll ( channelId, groups ); } } // methods of DeployAuthService @Override public List<DeployGroup> listGroups ( final int position, final int count ) { return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> { return split ( model.getDeployGroups (), position, count ); } ); } @Override public DeployGroup createGroup ( final String name ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { return model.createGroup ( name ); } ); } @Override public void deleteGroup ( final String groupId ) { try ( final Locked l = lock ( this.writeLock ) ) { this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> { model.deleteGroup ( groupId ); StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( model ); } ); } ); } } @Override public void updateGroup ( final String groupId, final String name ) { try ( final Locked l = lock ( this.writeLock ) ) { this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> { model.updateGroup ( groupId, name ); StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( model ); } ); } ); } } @Override public DeployGroup getGroup ( final String groupId ) { return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> { return model.getDeployGroup ( groupId ); } ); } @Override public DeployKey createDeployKey ( final String groupId, final String name ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( model ); } ); return model.createKey ( groupId, name ); } ); } @Override public DeployKey deleteDeployKey ( final String keyId ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( model ); } ); return model.deleteKey ( keyId ); } ); } @Override public DeployKey getDeployKey ( final String keyId ) { return this.manager.accessCall ( KEY_STORAGE, ChannelServiceAccess.class, model -> { return model.getDeployKey ( keyId ); } ); } @Override public DeployKey updateDeployKey ( final String keyId, final String name ) { return this.manager.modifyCall ( KEY_STORAGE, ChannelServiceModify.class, model -> { StorageManager.executeAfterPersist ( () -> { updateDeployGroupCache ( model ); } ); return model.updateKey ( keyId, name ); } ); } @Override public void wipeClean () { try ( Locked l = lock ( this.writeLock ) ) { this.manager.modifyRun ( KEY_STORAGE, ChannelServiceModify.class, model -> { wipeChannelService ( model ); } ); for ( final ChannelProvider provider : this.providerMap.values () ) { provider.wipe (); } } } private void wipeChannelService ( final ChannelServiceModify model ) { model.clear (); StorageManager.executeAfterPersist ( () -> { // wipe all names for ( final ChannelEntry entry : this.channelMap.values () ) { entry.setId ( new ChannelId ( entry.getId ().getId (), null ) ); } } ); } @Override public ChannelStatistics getStatistics () { final ChannelStatistics cs = new ChannelStatistics (); try ( Locked l = lock ( this.readLock ) ) { final Collection<ChannelInformation> cis = list (); cs.setTotalNumberOfArtifacts ( cis.stream ().mapToLong ( ci -> ci.getState ().getNumberOfArtifacts () ).sum () ); cs.setTotalNumberOfBytes ( cis.stream ().mapToLong ( ci -> ci.getState ().getNumberOfBytes () ).sum () ); } return cs; } }