/******************************************************************************* * 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.apm; import static java.util.stream.Collectors.toMap; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.UUID; import java.util.function.Consumer; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.channel.ArtifactInformation; import org.eclipse.packagedrone.repo.channel.ArtifactInformation.Manipulator; import org.eclipse.packagedrone.repo.channel.CacheEntry; import org.eclipse.packagedrone.repo.channel.CacheEntryInformation; import org.eclipse.packagedrone.repo.channel.ChannelDetails; import org.eclipse.packagedrone.repo.channel.ChannelState; import org.eclipse.packagedrone.repo.channel.ChannelState.Builder; import org.eclipse.packagedrone.repo.channel.IdTransformer; import org.eclipse.packagedrone.repo.channel.ValidationMessage; import org.eclipse.packagedrone.repo.channel.apm.aspect.AspectContextImpl; import org.eclipse.packagedrone.repo.channel.apm.aspect.AspectableContext; import org.eclipse.packagedrone.repo.channel.apm.internal.Activator; import org.eclipse.packagedrone.repo.channel.apm.store.BlobStore; import org.eclipse.packagedrone.repo.channel.apm.store.BlobStore.Transaction; import org.eclipse.packagedrone.repo.channel.apm.store.CacheStore; import org.eclipse.packagedrone.repo.channel.provider.ModifyContext; import org.eclipse.packagedrone.storage.apm.StorageManager; import org.eclipse.packagedrone.utils.Exceptions; import org.eclipse.packagedrone.utils.io.IOConsumer; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; public class ModifyContextImpl implements ModifyContext, AspectableContext { private static final String FACET_GENERATOR = "generator"; private final String localChannelId; private final EventAdmin eventAdmin; private final BlobStore store; private final CacheStore cacheStore; private final SortedMap<String, String> aspectStates; private final SortedMap<String, String> modAspectStates; private final Map<MetaKey, String> extractedMetadata; private final Map<MetaKey, String> modExtractedMetadata; private final Map<MetaKey, String> providedMetadata; private final Map<MetaKey, String> modProvidedMetadata; private final Map<String, ArtifactInformation> artifacts; private final Map<String, ArtifactInformation> modArtifacts; private final Map<String, ArtifactInformation> generatorArtifacts; private final Map<String, ArtifactInformation> modGeneratorArtifacts; private final Map<MetaKey, CacheEntryInformation> cacheEntries; private final Map<MetaKey, CacheEntryInformation> modCacheEntries; private final Builder state; private Transaction transaction; private CacheStore.Transaction cacheTransaction; private final AspectContextImpl aspectContext; private SortedMap<MetaKey, String> metaDataCache; private IdTransformer idTransformer; /** * Create a new empty modification context * * @param localChannelId * the local channel id * @param eventAdmin * the event admin to post events * @param store * the blob store * @param cacheStore * the cache store */ public ModifyContextImpl ( final String localChannelId, final EventAdmin eventAdmin, final BlobStore store, final CacheStore cacheStore ) { this.localChannelId = localChannelId; this.eventAdmin = eventAdmin; this.store = store; this.cacheStore = cacheStore; this.state = new Builder (); // main collections this.modAspectStates = new TreeMap<> (); this.modCacheEntries = new HashMap<> (); this.modArtifacts = new HashMap<> (); this.modGeneratorArtifacts = new HashMap<> (); this.modExtractedMetadata = new HashMap<> (); this.modProvidedMetadata = new HashMap<> (); // create unmodifiable collections this.aspectStates = Collections.unmodifiableSortedMap ( this.modAspectStates ); this.cacheEntries = Collections.unmodifiableMap ( this.modCacheEntries ); this.artifacts = Collections.unmodifiableMap ( this.modArtifacts ); this.generatorArtifacts = Collections.unmodifiableMap ( this.modGeneratorArtifacts ); this.extractedMetadata = Collections.unmodifiableMap ( this.modExtractedMetadata ); this.providedMetadata = Collections.unmodifiableMap ( this.modProvidedMetadata ); // aspect context this.aspectContext = new AspectContextImpl ( this, Activator.getProcessor () ); } public ModifyContextImpl ( final String localChannelId, final EventAdmin eventAdmin, final BlobStore store, final CacheStore cacheStore, final ChannelState state, final Map<String, String> aspectStates, final Map<String, ArtifactInformation> artifacts, final Map<MetaKey, CacheEntryInformation> cacheEntries, final Map<MetaKey, String> extractedMetaData, final Map<MetaKey, String> providedMetaData ) { this.localChannelId = localChannelId; this.eventAdmin = eventAdmin; this.store = store; this.cacheStore = cacheStore; this.state = new Builder ( state ); // main collections this.modAspectStates = new TreeMap<> ( aspectStates ); this.modCacheEntries = new HashMap<> ( cacheEntries ); this.modArtifacts = new HashMap<> ( artifacts ); this.modGeneratorArtifacts = this.modArtifacts.values ().stream ().filter ( art -> art.is ( FACET_GENERATOR ) ).collect ( toMap ( ArtifactInformation::getId, a -> a ) ); this.modExtractedMetadata = new HashMap<> ( extractedMetaData ); this.modProvidedMetadata = new HashMap<> ( providedMetaData ); // create unmodifiable collections this.aspectStates = Collections.unmodifiableSortedMap ( this.modAspectStates ); this.cacheEntries = Collections.unmodifiableMap ( this.modCacheEntries ); this.artifacts = Collections.unmodifiableMap ( this.modArtifacts ); this.generatorArtifacts = Collections.unmodifiableMap ( this.modGeneratorArtifacts ); this.extractedMetadata = Collections.unmodifiableMap ( this.modExtractedMetadata ); this.providedMetadata = Collections.unmodifiableMap ( this.modProvidedMetadata ); // aspect context this.aspectContext = new AspectContextImpl ( this, Activator.getProcessor () ); } public ModifyContextImpl ( final ModifyContextImpl other ) { this.localChannelId = other.localChannelId; this.eventAdmin = other.eventAdmin; this.store = other.store; this.cacheStore = other.cacheStore; this.state = new Builder ( other.state.build () ); // main collections this.modAspectStates = new TreeMap<> ( other.aspectStates ); this.modCacheEntries = new HashMap<> ( other.cacheEntries ); this.modArtifacts = new HashMap<> ( other.artifacts ); this.modGeneratorArtifacts = new HashMap<> ( other.generatorArtifacts ); this.modExtractedMetadata = new HashMap<> ( other.extractedMetadata ); this.modProvidedMetadata = new HashMap<> ( other.providedMetadata ); // create unmodifiable collections this.aspectStates = Collections.unmodifiableSortedMap ( this.modAspectStates ); this.cacheEntries = Collections.unmodifiableMap ( this.modCacheEntries ); this.artifacts = Collections.unmodifiableMap ( this.modArtifacts ); this.generatorArtifacts = Collections.unmodifiableMap ( this.modGeneratorArtifacts ); this.extractedMetadata = Collections.unmodifiableMap ( this.modExtractedMetadata ); this.providedMetadata = Collections.unmodifiableMap ( this.modProvidedMetadata ); // aspect context this.aspectContext = new AspectContextImpl ( this, Activator.getProcessor () ); } public void setIdTransformer ( final IdTransformer idTransformer ) { this.idTransformer = idTransformer; } @Override public ChannelState getState () { return this.state.build (); // will only create a new instance when necessary } @Override public String getChannelId () { if ( this.idTransformer == null ) { throw new IllegalStateException ( "'idTransformer' was not set, it is required to get the external channel id" ); } return this.idTransformer.transform ( this.localChannelId ); } @Override public SortedMap<MetaKey, String> getMetaData () { // TODO: check if this should be synchronized if ( this.metaDataCache == null ) { final TreeMap<MetaKey, String> tmp = new TreeMap<> (); if ( this.modExtractedMetadata != null ) { tmp.putAll ( this.modExtractedMetadata ); } if ( this.modProvidedMetadata != null ) { tmp.putAll ( this.modProvidedMetadata ); } this.metaDataCache = Collections.unmodifiableSortedMap ( tmp ); } return this.metaDataCache; } @Override public Map<String, ArtifactInformation> getArtifacts () { return this.artifacts; } @Override public Map<String, ArtifactInformation> getGeneratorArtifacts () { return this.generatorArtifacts; } @Override public void setDetails ( final ChannelDetails details ) { this.state.setDescription ( details.getDescription () ); markModified (); } @Override public Map<MetaKey, CacheEntryInformation> getCacheEntries () { return this.cacheEntries; } @Override public SortedMap<String, String> getAspectStates () { return this.aspectStates; } @Override public SortedMap<String, String> getModifiableAspectStates () { return this.modAspectStates; } @Override public void applyMetaData ( final Map<MetaKey, String> changes ) { testLocked (); for ( final Map.Entry<MetaKey, String> entry : changes.entrySet () ) { final MetaKey key = entry.getKey (); final String value = entry.getValue (); if ( value == null ) { this.modProvidedMetadata.remove ( key ); } else { this.modProvidedMetadata.put ( key, value ); } } // clear cache this.metaDataCache = null; // mark modified markModified (); // re-aggregate this.aspectContext.aggregate (); } @Override public void applyMetaData ( final String artifactId, final Map<MetaKey, String> changes ) { testLocked (); final ArtifactInformation artifact = this.modArtifacts.get ( artifactId ); if ( artifact == null ) { throw new IllegalStateException ( String.format ( "Artifact '%s' is unknown", artifactId ) ); } if ( !artifact.getFacets ().contains ( "stored" ) ) { throw new IllegalStateException ( String.format ( "Artifact '%s' is not 'stored'", artifactId ) ); } final Manipulator m = artifact.createManipulator (); for ( final Map.Entry<MetaKey, String> entry : changes.entrySet () ) { final MetaKey key = entry.getKey (); final String value = entry.getValue (); if ( value == null ) { m.getProvidedMetaData ().remove ( key ); } else { m.getProvidedMetaData ().put ( key, value ); } } // update updateArtifact ( m ); // mark modified markModified (); // regenerate generators if ( artifact.getFacets ().contains ( FACET_GENERATOR ) ) { this.aspectContext.regenerate ( artifactId ); } // TODO: new behavior - regenerate normal artifacts since this might have changed virtual artifacts } private void testLocked () { if ( this.state.build ().isLocked () ) { throw new IllegalStateException ( "Channel is locked" ); } } @Override public void lock () { this.state.setLocked ( true ); } @Override public void unlock () { this.state.setLocked ( false ); } private void ensureTransaction () { if ( this.transaction == null ) { this.transaction = this.store.start (); } } private void ensureCacheTransaction () { if ( this.cacheTransaction == null ) { this.cacheTransaction = this.cacheStore.startTransaction (); this.modCacheEntries.clear (); } } public Transaction claimTransaction () { final Transaction t = this.transaction; this.transaction = null; return t; } public CacheStore.Transaction claimCacheTransaction () { final CacheStore.Transaction t = this.cacheTransaction; this.cacheTransaction = null; return t; } @Override public ArtifactInformation createArtifact ( final InputStream source, final String name, final Map<MetaKey, String> providedMetaData ) { return createArtifact ( null, source, name, providedMetaData ); } @Override public ArtifactInformation createArtifact ( final String parentId, final InputStream source, final String name, final Map<MetaKey, String> providedMetaData ) { testLocked (); markModified (); if ( parentId != null ) { final ArtifactInformation parent = this.modArtifacts.get ( parentId ); if ( parent != null ) // we only check if the parent is there, a missing parent will be checked later on { if ( !parent.is ( "stored" ) ) { throw new IllegalArgumentException ( String.format ( "Unable to use artifact %s as parent, it is not a stored artifact", parentId ) ); } } } return Exceptions.wrapException ( () -> this.aspectContext.createArtifact ( parentId, source, name, providedMetaData ) ); } @Override public ArtifactInformation createGeneratorArtifact ( final String generatorId, final InputStream source, final String name, final Map<MetaKey, String> providedMetaData ) { testLocked (); markModified (); return Exceptions.wrapException ( () -> this.aspectContext.createGeneratorArtifact ( generatorId, source, name, providedMetaData ) ); } @Override public ArtifactInformation createPlainArtifact ( final String parentId, final InputStream source, final String name, final Map<MetaKey, String> providedMetaData, final Set<String> facets, final String virtualizerAspectId ) { ensureTransaction (); markModified (); final String id = UUID.randomUUID ().toString (); try { final long size = this.transaction.create ( id, source ); final ArtifactInformation parent; // validate parent if ( parentId != null ) { parent = this.artifacts.get ( parentId ); if ( parent == null ) { throw new IllegalArgumentException ( String.format ( "Parent artifact %s does not exists", parentId ) ); } } else { parent = null; } final ArtifactInformation ai = new ArtifactInformation ( id, parentId, Collections.emptySet (), name, size, Instant.now (), facets, Collections.emptyList (), providedMetaData, null, virtualizerAspectId ); this.modArtifacts.put ( ai.getId (), ai ); if ( ai.is ( FACET_GENERATOR ) ) { this.modGeneratorArtifacts.put ( ai.getId (), ai ); } if ( parent != null ) { // add as child final Manipulator m = parent.createManipulator (); m.getChildIds ().add ( ai.getId () ); updateArtifact ( m ); } // refresh number of artifacts this.state.setNumberOfArtifacts ( this.modArtifacts.size () ); this.state.incrementNumberOfBytes ( ai.getSize () ); return ai; } catch ( final IOException e ) { throw new RuntimeException ( "Failed to create artifact", e ); } } private ArtifactInformation updateArtifact ( final Manipulator manipulator ) { markModified (); final ArtifactInformation art = manipulator.build (); this.modArtifacts.put ( art.getId (), art ); return art; } private boolean internalDeleteArtifact ( final String id ) throws IOException { ensureTransaction (); final boolean result = this.transaction.delete ( id ); final ArtifactInformation ai = this.modArtifacts.remove ( id ); if ( ai == null ) { return result; } // remove from generators this.modGeneratorArtifacts.remove ( id ); // remove children as well if ( ai.getChildIds () != null ) { for ( final String childId : ai.getChildIds () ) { internalDeleteArtifact ( childId ); } } // remove from parent's child list if ( ai.getParentId () != null ) { final ArtifactInformation parent = this.modArtifacts.get ( ai.getParentId () ); if ( parent != null ) { final Manipulator m = parent.createManipulator (); m.getChildIds ().remove ( id ); updateArtifact ( m ); } } // remove from generators if ( ai.is ( FACET_GENERATOR ) ) { this.modGeneratorArtifacts.remove ( id ); } // refresh number of artifacts this.state.setNumberOfArtifacts ( this.modArtifacts.size () ); this.state.incrementNumberOfBytes ( -ai.getSize () ); return result; } @Override public ArtifactInformation deletePlainArtifact ( final String id ) { markModified (); try { final ArtifactInformation artifact = this.modArtifacts.get ( id ); if ( artifact == null ) { return null; } final ArtifactInformation result = internalDeleteArtifact ( id ) ? artifact : null; return result; } catch ( final IOException e ) { throw new RuntimeException ( "Failed to delete artifact", e ); } } @Override public boolean deleteArtifact ( final String id ) { testLocked (); markModified (); ensureTransaction (); final ArtifactInformation artifact = this.modArtifacts.get ( id ); if ( artifact == null ) { return false; } if ( !artifact.is ( "stored" ) ) { throw new IllegalStateException ( String.format ( "Unable to delete artifact '%s'. It is not 'stored'.", id ) ); } final boolean result = this.aspectContext.deleteArtifacts ( Collections.singleton ( id ) ); // no need to refresh return result; } @Override public boolean stream ( final String artifactId, final IOConsumer<InputStream> consumer ) throws IOException { if ( this.transaction != null ) { // stream from transaction return this.transaction.stream ( artifactId, consumer ); } else { // stream from store return this.store.stream ( artifactId, consumer ); } } @Override public boolean streamCacheEntry ( final MetaKey key, final IOConsumer<CacheEntry> consumer ) throws IOException { final CacheEntryInformation entry = this.cacheEntries.get ( key ); if ( entry == null ) { return false; } if ( this.cacheTransaction != null ) { // stream from transaction return this.cacheTransaction.stream ( key, stream -> { consumer.accept ( new CacheEntry ( entry, stream ) ); } ); } else { // stream from store return this.cacheStore.stream ( key, stream -> { consumer.accept ( new CacheEntry ( entry, stream ) ); } ); } } @Override public void clear () { testLocked (); markModified (); ensureTransaction (); ensureCacheTransaction (); final String[] keys = this.modArtifacts.keySet ().toArray ( new String[this.modArtifacts.size ()] ); for ( final String art : keys ) { try { internalDeleteArtifact ( art ); } catch ( final IOException e ) { throw new RuntimeException ( "Failed to delete artifact: " + art, e ); } } // clear generators this.modGeneratorArtifacts.clear (); // clear cache entries this.modCacheEntries.clear (); Exceptions.wrapException ( () -> this.cacheTransaction.clear () ); // clear extracted channel meta data this.modExtractedMetadata.clear (); // clear meta data cache this.metaDataCache = null; // clear validation messages this.state.setValidationMessages ( Collections.emptyList () ); // refresh number of artifacts this.state.setNumberOfArtifacts ( 0L ); this.state.setNumberOfBytes ( 0L ); } @Override public void addAspects ( final Set<String> aspectIds ) { testLocked (); markModified (); this.aspectContext.addAspects ( aspectIds ); } @Override public void removeAspects ( final Set<String> aspectIds ) { Objects.requireNonNull ( aspectIds, "'aspectIds' must not be null" ); testLocked (); markModified (); this.aspectContext.removeAspects ( aspectIds ); postAspectEvents ( aspectIds, "remove" ); } @Override public void refreshAspects ( Set<String> aspectIds ) { testLocked (); markModified (); if ( aspectIds == null ) { aspectIds = new HashSet<> ( this.aspectStates.keySet () ); } this.aspectContext.refreshAspects ( aspectIds ); postAspectEvents ( aspectIds, "refresh" ); } protected ArtifactInformation modifyArtifact ( final String artifactId, final Consumer<Manipulator> modification ) { final ArtifactInformation art = this.artifacts.get ( artifactId ); if ( art == null ) { throw new IllegalStateException ( String.format ( "Unable to find artifact '%s'", artifactId ) ); } // perform modification final Manipulator m = art.createManipulator (); modification.accept ( m ); // mark modified markModified (); // update from the model return updateArtifact ( m ); } @Override public ArtifactInformation setExtractedMetaData ( final String artifactId, final Map<MetaKey, String> metaData ) { return modifyArtifact ( artifactId, art -> { // set the extracted data art.setExtractedMetaData ( new HashMap<> ( metaData ) ); } ); } @Override public ArtifactInformation setValidationMessages ( final String artifactId, final List<ValidationMessage> messages ) { return modifyArtifact ( artifactId, art -> { // set validation messages art.setValidationMessages ( messages ); } ); } @Override public void setExtractedMetaData ( final Map<MetaKey, String> metaData ) { this.modExtractedMetadata.clear (); this.modExtractedMetadata.putAll ( metaData ); this.metaDataCache = null; markModified (); } @Override public void setValidationMessages ( final List<ValidationMessage> messages ) { this.state.setValidationMessages ( messages ); markModified (); } @Override public Collection<ValidationMessage> getValidationMessages () { return Collections.unmodifiableCollection ( this.state.build ().getValidationMessages () ); } @Override public void regenerate ( final String artifactId ) { testLocked (); this.aspectContext.regenerate ( artifactId ); markModified (); } @Override public Map<MetaKey, String> getChannelProvidedMetaData () { return this.providedMetadata; } @Override public Map<MetaKey, String> getProvidedMetaData () { return getChannelProvidedMetaData (); } @Override public Map<MetaKey, String> getExtractedMetaData () { return this.extractedMetadata; } @Override public void createCacheEntry ( final MetaKey key, final String name, final String mimeType, final IOConsumer<OutputStream> creator ) throws IOException { ensureCacheTransaction (); final long size = this.cacheTransaction.put ( key, creator ); final CacheEntryInformation entry = new CacheEntryInformation ( key, name, size, mimeType, Instant.now () ); this.modCacheEntries.put ( key, entry ); markModified (); } @Override public ChannelDetails getChannelDetails () { final ChannelDetails result = new ChannelDetails (); result.setDescription ( this.state.build ().getDescription () ); return result; } protected void postAspectEvents ( final Set<String> aspectIds, final String operation ) { final String channelId = getChannelId (); StorageManager.executeAfterPersist ( () -> { for ( final String aspectFactoryId : aspectIds ) { postAspectEvent ( channelId, aspectFactoryId, operation ); } } ); } protected void postAspectEvent ( final String channelId, final String aspectId, final String operation ) { if ( this.eventAdmin != null ) { final Map<String, Object> data = new HashMap<> ( 2 ); data.put ( "operation", operation ); data.put ( "aspectFactoryId", aspectId ); this.eventAdmin.postEvent ( new Event ( String.format ( "drone/channel/%s/aspect", makeSafeTopic ( channelId ) ), data ) ); } } private static String makeSafeTopic ( final String aspectId ) { return aspectId.replaceAll ( "[^a-zA-Z0-9_\\-]", "_" ); } private void markModified () { final Instant now = Instant.now (); this.state.setModificationTimestamp ( now ); } }