/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.beam.runners.core.construction;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.io.BaseEncoding;
import io.grpc.stub.StreamObserver;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi.ArtifactMetadata;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi.CommitManifestResponse;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi.Manifest;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi.PutArtifactRequest;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi.PutArtifactRequest.ContentCase;
import org.apache.beam.model.jobmanagement.v1.ArtifactApi.PutArtifactResponse;
import org.apache.beam.model.jobmanagement.v1.ArtifactStagingServiceGrpc.ArtifactStagingServiceImplBase;

/**
 * An {@link ArtifactStagingServiceImplBase ArtifactStagingService} which stores the bytes of the
 * artifacts in memory..
 */
public class InMemoryArtifactStagerService extends ArtifactStagingServiceImplBase {
  private final Map<ArtifactMetadata, byte[]> artifactBytes;
  private Manifest manifest;

  public InMemoryArtifactStagerService() {
    artifactBytes = new HashMap<>();
  }

  @Override
  public StreamObserver<ArtifactApi.PutArtifactRequest> putArtifact(
      StreamObserver<ArtifactApi.PutArtifactResponse> responseObserver) {
    return new BufferingObserver(responseObserver);
  }

  @Override
  public void commitManifest(
      ArtifactApi.CommitManifestRequest request,
      StreamObserver<ArtifactApi.CommitManifestResponse> responseObserver) {
    this.manifest = request.getManifest();
    responseObserver.onNext(CommitManifestResponse.getDefaultInstance());
    responseObserver.onCompleted();
  }

  public Map<ArtifactMetadata, byte[]> getStagedArtifacts() {
    return Collections.unmodifiableMap(artifactBytes);
  }

  public Manifest getManifest() {
    return manifest;
  }

  private class BufferingObserver implements StreamObserver<PutArtifactRequest> {
    private final StreamObserver<PutArtifactResponse> responseObserver;
    private ArtifactMetadata destination = null;
    private BufferWritingObserver writer = null;

    public BufferingObserver(StreamObserver<PutArtifactResponse> responseObserver) {
      this.responseObserver = responseObserver;
    }

    @Override
    public void onNext(PutArtifactRequest value) {
      if (writer == null) {
        checkArgument(value.getContentCase().equals(ContentCase.METADATA));
        writer = new BufferWritingObserver();
        destination = value.getMetadata();
      } else {
        writer.onNext(value);
      }
    }

    @Override
    public void onError(Throwable t) {
      if (writer != null) {
        writer.onError(t);
      }
      onCompleted();
    }

    @Override
    public void onCompleted() {
      if (writer != null) {
        writer.onCompleted();
        try {
          artifactBytes.put(
              destination
                  .toBuilder()
                  .setMd5(
                      BaseEncoding.base64()
                          .encode(
                              MessageDigest.getInstance("MD5").digest(writer.stream.toByteArray())))
                  .build(),
              writer.stream.toByteArray());
        } catch (NoSuchAlgorithmException e) {
          throw new AssertionError("The Java Spec requires all JVMs to support MD5", e);
        }
      }
      responseObserver.onNext(PutArtifactResponse.getDefaultInstance());
      responseObserver.onCompleted();
    }
  }

  private static class BufferWritingObserver implements StreamObserver<PutArtifactRequest> {
    private final ByteArrayOutputStream stream;

    BufferWritingObserver() {
      stream = new ByteArrayOutputStream();
    }

    @Override
    public void onNext(PutArtifactRequest value) {
      try {
        stream.write(value.getData().getData().toByteArray());
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public void onError(Throwable t) {
      onCompleted();
    }

    @Override
    public void onCompleted() {
    }
  }
}

@@ -917,16 +917,79 @@
 rgument;
+%0Aimport static com.google.common.base.Preconditions.checkState;
 %0A%0Aimport
@@ -1260,37 +1260,153 @@
 til.
-HashMap;%0Aimport java.util.Map
+Map;%0Aimport java.util.concurrent.ConcurrentHashMap;%0Aimport java.util.concurrent.ConcurrentMap;%0Aimport java.util.concurrent.atomic.AtomicReference
 ;%0Aim
@@ -2272,16 +2272,26 @@
 e final 
+Concurrent
 Map%3CArti
@@ -2337,24 +2337,46 @@
 private 
+final AtomicReference%3C
 Manifest
  manifes
@@ -2367,16 +2367,17 @@
 Manifest
+%3E
  manifes
@@ -2377,16 +2377,42 @@
 manifest
+ = new AtomicReference%3C%3E()
 ;%0A%0A  pub
@@ -2473,16 +2473,26 @@
 s = new 
+Concurrent
 HashMap%3C
@@ -2889,44 +2889,185 @@
     
-this.manifest = request.getManifest(
+checkState(%0A        this.manifest.compareAndSet(null, request.getManifest()),%0A        %22Already committed a %25s %25s%22,%0A        Manifest.class.getSimpleName(),%0A        manifest.get()
 );%0A 
@@ -3355,16 +3355,22 @@
 manifest
+.get()
 ;%0A  %7D%0A%0A 

Use Conccurrent Constructs in InMemoryArtifactStagerService