/*******************************************************************************
* 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 java.io.BufferedInputStream;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.repo.channel.apm.internal.Finally;
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.AccessContext;
import org.eclipse.packagedrone.storage.apm.AbstractSimpleStorageModelProvider;
import org.eclipse.packagedrone.storage.apm.StorageContext;
import org.eclipse.packagedrone.storage.apm.util.ReplaceOnCloseOutputStream;
import org.eclipse.packagedrone.utils.profiler.Profile;
import org.eclipse.packagedrone.utils.profiler.Profile.Handle;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.LongSerializationPolicy;
public class ChannelModelProvider extends AbstractSimpleStorageModelProvider<AccessContext, ModifyContextImpl>
{
private final static Logger logger = LoggerFactory.getLogger ( ChannelModelProvider.class );
private final String channelId;
private BlobStore store;
private CacheStore cacheStore;
private final EventAdmin eventAdmin;
public ChannelModelProvider ( final EventAdmin eventAdmin, final String channelId )
{
super ( AccessContext.class, ModifyContextImpl.class );
this.eventAdmin = eventAdmin;
this.channelId = channelId;
}
@Override
public void start ( final StorageContext context ) throws Exception
{
this.store = new BlobStore ( makeBasePath ( context, this.channelId ).resolve ( "blobs" ) );
this.cacheStore = new CacheStore ( makeBasePath ( context, this.channelId ).resolve ( "cache" ) );
super.start ( context );
}
@Override
public void stop ()
{
super.stop ();
this.store.close ();
this.cacheStore.close ();
}
@Override
protected AccessContext makeViewModelTyped ( final ModifyContextImpl writeModel )
{
return writeModel;
}
@Override
protected ModifyContextImpl cloneWriteModel ( final ModifyContextImpl writeModel )
{
return new ModifyContextImpl ( writeModel );
}
public static Path makeBasePath ( final StorageContext context, final String channelId )
{
return context.getBasePath ().resolve ( Paths.get ( "channels", channelId ) );
}
public static Path makeStatePath ( final StorageContext context, final String channelId )
{
return makeBasePath ( context, channelId ).resolve ( "state.json" );
}
static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
static Gson createGson ()
{
final GsonBuilder builder = new GsonBuilder ();
builder.setPrettyPrinting ();
builder.setLongSerializationPolicy ( LongSerializationPolicy.STRING );
builder.setDateFormat ( DATE_FORMAT );
builder.registerTypeAdapter ( MetaKey.class, new JsonDeserializer<MetaKey> () {
@Override
public MetaKey deserialize ( final JsonElement json, final Type type, final JsonDeserializationContext ctx ) throws JsonParseException
{
return MetaKey.fromString ( json.getAsString () );
}
} );
return builder.create ();
}
@Override
protected void persistWriteModel ( final StorageContext context, final ModifyContextImpl writeModel ) throws Exception
{
try ( Handle h1 = Profile.start ( this, "persistWriteModel" ) )
{
final AtomicReference<Transaction> t = new AtomicReference<> ( writeModel.claimTransaction () );
final AtomicReference<CacheStore.Transaction> ct = new AtomicReference<> ( writeModel.claimCacheTransaction () );
final Finally f = new Finally ();
f.add ( () -> {
final Transaction v = t.get ();
if ( v != null )
{
v.rollback ();
}
} );
f.add ( () -> {
final CacheStore.Transaction v = ct.get ();
if ( v != null )
{
v.rollback ();
}
} );
try
{
final Path path = makeStatePath ( context, this.channelId );
Files.createDirectories ( path.getParent () );
// commit blob store
if ( t.get () != null )
{
t.get ().commit ();
t.set ( null );
}
// write model
try ( Handle h2 = Profile.start ( this, "persistWriteModel#write" ) )
{
try ( ReplaceOnCloseOutputStream stream = new ReplaceOnCloseOutputStream ( path );
ChannelWriter writer = new ChannelWriter ( stream ); )
{
writer.write ( writeModel );
stream.commit ();
}
}
// commit cache store
if ( ct.get () != null )
{
ct.get ().commit ();
ct.set ( null );
}
}
catch ( final Exception e )
{
logger.warn ( "Failed to persist model", e );
throw e;
}
finally
{
try ( Handle h2 = Profile.start ( this, "persistWriteModel#finally" ) )
{
f.runAll ();
}
}
}
}
@Override
protected ModifyContextImpl loadWriteModel ( final StorageContext context ) throws Exception
{
final Path path = makeStatePath ( context, this.channelId );
try ( InputStream stream = new BufferedInputStream ( Files.newInputStream ( path ) );
ChannelReader reader = new ChannelReader ( stream, this.channelId, this.eventAdmin, this.store, this.cacheStore ); )
{
final ModifyContextImpl model = reader.read ();
if ( model == null )
{
// FIXME: handle broken channel state
throw new IllegalStateException ( "Unable to load channel model" );
}
return model;
}
catch ( final NoSuchFileException e )
{
// create a new model
final ChannelModel model = new ChannelModel ();
model.setCreationTimestamp ( new Date () );
return new ModifyContextImpl ( this.channelId, this.eventAdmin, this.store, this.cacheStore );
}
}
}