/* Licensed 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.camunda.bpm.engine.test.concurrency;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import java.io.ByteArrayOutputStream;
import java.util.List;

import org.camunda.bpm.engine.impl.cmd.DeployCmd;
import org.camunda.bpm.engine.impl.interceptor.CommandContext;
import org.camunda.bpm.engine.impl.repository.DeploymentBuilderImpl;
import org.camunda.bpm.engine.repository.Deployment;
import org.camunda.bpm.engine.repository.DeploymentBuilder;
import org.camunda.bpm.engine.repository.DeploymentQuery;
import org.camunda.bpm.engine.repository.ProcessDefinition;
import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;

/**
 * <p>Tests the deployment from two threads simultaneously.</p>
 *
 * <p><b>Note:</b> the tests are not execute on H2 because it doesn't support the
 * exclusive lock on the deployment table.</p>
 *
 * @author Daniel Meyer
 */
public class ConcurrentDeploymentTest extends ConcurrencyTestCase {

  private static String processResource;

  static {
    BpmnModelInstance modelInstance = Bpmn.createExecutableProcess().startEvent().done();
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    Bpmn.writeModelToStream(outputStream, modelInstance);
    processResource = new String(outputStream.toByteArray());
  }

  /**
   * hook into test method invocation - after the process engine is initialized
   */
  @Override
  protected void runTest() throws Throwable {
    String databaseType = processEngineConfiguration.getDbSqlSessionFactory().getDatabaseType();

    if("h2".equals(databaseType)) {
      // skip test method - if database is H2
    } else {
      // invoke the test method
      super.runTest();
    }
  }

  /**
   * @see https://app.camunda.com/jira/browse/CAM-2128
   */
  public void testDuplicateFiltering() throws InterruptedException {

    DeploymentBuilder deployment = new DeploymentBuilderImpl(null)
        .name("some-deployment-name")
        .addString("foo.bpmn", processResource)
        .enableDuplicateFiltering(false);

    deployOnTwoConcurrentThreads(deployment);

    // ensure that although both transactions were run concurrently, only one deployment was constructed.
    DeploymentQuery deploymentQuery = repositoryService.createDeploymentQuery();
    assertThat(deploymentQuery.count(), is(1L));
  }

  public void testVersioning() throws InterruptedException {

    DeploymentBuilder deployment = new DeploymentBuilderImpl(null)
        .name("some-deployment-name")
        .addString("foo.bpmn", processResource);

    deployOnTwoConcurrentThreads(deployment);

    // ensure that although both transactions were run concurrently, the process definitions have different versions
    List<ProcessDefinition> processDefinitions = repositoryService
        .createProcessDefinitionQuery()
        .orderByProcessDefinitionVersion()
        .asc()
        .list();

    assertThat(processDefinitions.size(), is(2));
    assertThat(processDefinitions.get(0).getVersion(), is(1));
    assertThat(processDefinitions.get(1).getVersion(), is(2));
  }

  protected void deployOnTwoConcurrentThreads(DeploymentBuilder deploymentBuilder) throws InterruptedException {
    // STEP 1: bring two threads to a point where they have
    // 1) started a new transaction
    // 2) are ready to deploy
    ThreadControl thread1 = executeControllableCommand(new ControllableDeployCommand(deploymentBuilder));
    thread1.waitForSync();

    ThreadControl thread2 = executeControllableCommand(new ControllableDeployCommand(deploymentBuilder));
    thread2.waitForSync();

    // STEP 2: make Thread 1 proceed and wait until it has deployed but not yet committed
    // -> will still hold the exclusive lock
    thread1.makeContinue();
    thread1.waitForSync();

    // STEP 3: make Thread 2 continue
    // -> it will attempt to acquire the exclusive lock and block on the lock
    thread2.makeContinue();

    // wait for 2 seconds (Thread 2 is blocked on the lock)
    Thread.sleep(2000);

    // STEP 4: allow Thread 1 to terminate
    // -> Thread 1 will commit and release the lock
    thread1.waitUntilDone();

    // STEP 5: wait for Thread 2 to terminate
    thread2.waitForSync();
    thread2.waitUntilDone();
  }

  @Override
  protected void tearDown() throws Exception {

    for(Deployment deployment : repositoryService.createDeploymentQuery().list()) {
      repositoryService.deleteDeployment(deployment.getId(), true);
    }
  }

  protected static class ControllableDeployCommand extends ControllableCommand<Void> {

    private final DeploymentBuilder deploymentBuilder;

    public ControllableDeployCommand(DeploymentBuilder deploymentBuilder) {
      this.deploymentBuilder = deploymentBuilder;
    }

    public Void execute(CommandContext commandContext) {
      monitor.sync();  // thread will block here until makeContinue() is called form main thread

      new DeployCmd<Deployment>((DeploymentBuilderImpl) deploymentBuilder).execute(commandContext);

      monitor.sync();  // thread will block here until waitUntilDone() is called form main thread

      return null;
    }

  }

}

@@ -647,16 +647,61 @@
 ers.is;%0A
+import static org.hamcrest.CoreMatchers.not;%0A
 import s
@@ -2497,243 +2497,170 @@
     
-D
+d
 eploy
-mentBuilder deployment = new DeploymentBuilderImpl(null)%0A        .name(%22some-deployment-name%22)%0A        .addString(%22foo.bpmn%22, processResource)%0A        .enableDuplicateFiltering(false);%0A%0A    deployOnTwoConcurrentThreads(deployment
+OnTwoConcurrentThreads(%0A        createDeploymentBuilder().enableDuplicateFiltering(false),%0A        createDeploymentBuilder().enableDuplicateFiltering(false)
 );%0A%0A
@@ -2970,92 +2970,88 @@
     
-D
+d
 eploy
-mentBuilder deployment = new DeploymentBuilderImpl(null)%0A        .name(%22some-d
+OnTwoConcurrentThreads(%0A        createDeploymentBuilder(),%0A        createD
 eplo
@@ -3059,14 +3059,16 @@
 ment
--name%22
+Builder(
 )%0A  
@@ -3077,93 +3077,8 @@
     
-.addString(%22foo.bpmn%22, processResource);%0A%0A    deployOnTwoConcurrentThreads(deployment
 );%0A%0A
@@ -3574,108 +3574,454 @@
 ted 
-void deployOnTwoConcurrentThreads(DeploymentBuilder deploymentBuilder) throws InterruptedException %7B
+DeploymentBuilder createDeploymentBuilder() %7B%0A    return new DeploymentBuilderImpl(null)%0A        .name(%22some-deployment-name%22)%0A        .addString(%22foo.bpmn%22, processResource);%0A  %7D%0A%0A  protected void deployOnTwoConcurrentThreads(DeploymentBuilder deploymentOne, DeploymentBuilder deploymentTwo) throws InterruptedException %7B%0A    assertThat(%22you can not use the same deployment builder for both deployments%22, deploymentOne, is(not(deploymentTwo)));%0A
 %0A   
@@ -4230,39 +4230,35 @@
 mmand(deployment
-Builder
+One
 ));%0A    thread1.
@@ -4368,23 +4368,19 @@
 ployment
-Builder
+Two
 ));%0A    

fix concurrent deployment test