/**
* Licensed to the Austrian Association for Software Tool Integration (AASTI)
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. The AASTI 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.openengsb.core.ekb.persistence.persist.edb.internal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.openengsb.core.api.context.ContextHolder;
import org.openengsb.core.edb.api.EDBCheckException;
import org.openengsb.core.edb.api.EDBCommit;
import org.openengsb.core.edb.api.EDBConstants;
import org.openengsb.core.edb.api.EDBException;
import org.openengsb.core.edb.api.EDBObject;
import org.openengsb.core.edb.api.EngineeringDatabaseService;
import org.openengsb.core.ekb.api.EKBCommit;
import org.openengsb.core.ekb.api.EKBConcurrentException;
import org.openengsb.core.ekb.api.EKBException;
import org.openengsb.core.ekb.api.ModelPersistException;
import org.openengsb.core.ekb.api.PersistInterface;
import org.openengsb.core.ekb.api.SanityCheckException;
import org.openengsb.core.ekb.api.SanityCheckReport;
import org.openengsb.core.ekb.api.hooks.EKBErrorHook;
import org.openengsb.core.ekb.api.hooks.EKBPostCommitHook;
import org.openengsb.core.ekb.api.hooks.EKBPreCommitHook;
import org.openengsb.core.ekb.common.ConvertedCommit;
import org.openengsb.core.ekb.common.EDBConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
/**
* Implementation of the PersistInterface service. It's main responsibilities are the saving of models and the sanity
* checks of these.
*/
public class PersistInterfaceService implements PersistInterface {
private static final Logger LOGGER = LoggerFactory.getLogger(PersistInterfaceService.class);
private final EngineeringDatabaseService edbService;
private final EDBConverter edbConverter;
private final List<EKBPreCommitHook> preCommitHooks;
private final List<EKBPostCommitHook> postCommitHooks;
private final List<EKBErrorHook> errorHooks;
private ContextLockingMode mode;
private final Set<String> activeWritingContexts;
public PersistInterfaceService(EngineeringDatabaseService edbService, EDBConverter edbConverter,
List<EKBPreCommitHook> preCommitHooks, List<EKBPostCommitHook> postCommitHooks,
List<EKBErrorHook> errorHooks, String contextLockingMode) {
this.edbService = edbService;
this.edbConverter = edbConverter;
this.preCommitHooks = preCommitHooks;
this.postCommitHooks = postCommitHooks;
this.errorHooks = errorHooks;
this.activeWritingContexts = new HashSet<String>();
try {
this.mode = ContextLockingMode.valueOf(contextLockingMode);
} catch (IllegalArgumentException e) {
this.mode = ContextLockingMode.DEACTIVATED;
LOGGER.error("Unknown mode setting. The context locking mechanism will be deactivated.", e);
}
}
@Override
public void commit(EKBCommit commit) throws SanityCheckException, EKBException {
LOGGER.debug("Commit of models was called");
runPersistingLogic(commit, true, null, false);
LOGGER.debug("Commit of models was successful");
}
@Override
public void commit(EKBCommit commit, UUID expectedContextHeadRevision) throws SanityCheckException, EKBException {
LOGGER.debug("Commit of models was called with the expected context head revision {}.",
expectedContextHeadRevision);
runPersistingLogic(commit, true, expectedContextHeadRevision, true);
LOGGER.debug("Commit of models was successful");
}
@Override
public void forceCommit(EKBCommit commit) throws EKBException {
LOGGER.debug("Force commit of models was called");
runPersistingLogic(commit, false, null, false);
LOGGER.debug("Force commit of models was successful");
}
@Override
public void forceCommit(EKBCommit commit, UUID expectedContextHeadRevision) throws EKBException {
LOGGER.debug("Force commit of models was called with the expected context head revision {}.",
expectedContextHeadRevision);
runPersistingLogic(commit, false, expectedContextHeadRevision, true);
LOGGER.debug("Force commit of models was successful");
}
/**
* Runs the logic of the PersistInterface. Does the sanity checks if check is set to true. Additionally tests if the
* head revision of the context under which the commit is performed has the given revision number if the
* headRevisionCheck flag is set to true.
*/
private void runPersistingLogic(EKBCommit commit, boolean check, UUID expectedContextHeadRevision,
boolean headRevisionCheck) throws SanityCheckException, EKBException {
String contextId = ContextHolder.get().getCurrentContextId();
try {
lockContext(contextId);
if (headRevisionCheck) {
checkForContextHeadRevision(contextId, expectedContextHeadRevision);
}
runEKBPreCommitHooks(commit);
if (check) {
performSanityChecks(commit);
}
EKBException exception = null;
ConvertedCommit converted = edbConverter.convertEKBCommit(commit);
try {
performPersisting(converted, commit);
runEKBPostCommitHooks(commit);
} catch (EKBException e) {
exception = e;
}
runEKBErrorHooks(commit, exception);
} finally {
releaseContext(contextId);
}
}
@Override
public SanityCheckReport check(EKBCommit commit) throws SanityCheckException, EKBException {
LOGGER.debug("Sanity checks of models was called");
SanityCheckReport report = performSanityChecks(commit);
LOGGER.debug("Sanity checks of models passed successful");
return report;
}
@Override
public void revertCommit(String revision) throws EKBException {
LOGGER.debug("Perform revert for the revision {}.", revision);
performRevertLogic(revision, null, false);
LOGGER.debug("Finished reverting the commit with the revision {}.", revision);
}
@Override
public void revertCommit(String revision, UUID expectedContextHeadRevision) throws EKBException {
LOGGER.debug("Perform revert for the revision {} and the expected context head revision {}.", revision,
expectedContextHeadRevision);
performRevertLogic(revision, expectedContextHeadRevision, true);
LOGGER.debug("Finished reverting the commit with the revision {}.", revision);
}
/**
* Performs the actual revert logic including the context locking and the context head revision check if desired.
*/
private void performRevertLogic(String revision, UUID expectedContextHeadRevision, boolean expectedHeadCheck) {
String contextId = "";
try {
EDBCommit commit = edbService.getCommitByRevision(revision);
contextId = commit.getContextId();
lockContext(contextId);
if (expectedHeadCheck) {
checkForContextHeadRevision(contextId, expectedContextHeadRevision);
}
EDBCommit newCommit = edbService.createEDBCommit(new ArrayList<EDBObject>(),
new ArrayList<EDBObject>(), new ArrayList<EDBObject>());
for (EDBObject reverted : commit.getObjects()) {
// need to be done in order to avoid problems with conflict detection
reverted.remove(EDBConstants.MODEL_VERSION);
newCommit.update(reverted);
}
for (String delete : commit.getDeletions()) {
newCommit.delete(delete);
}
newCommit.setComment(String.format("revert [%s] %s", commit.getRevisionNumber().toString(),
commit.getComment() != null ? commit.getComment() : ""));
edbService.commit(newCommit);
} catch (EDBException e) {
throw new EKBException("Unable to revert to the given revision " + revision, e);
} finally {
releaseContext(contextId);
}
}
/**
* If the context locking mode is activated, this method locks the given context for writing operations. If this
* context is already locked, an EKBConcurrentException is thrown.
*/
private void lockContext(String contextId) throws EKBConcurrentException {
if (mode == ContextLockingMode.DEACTIVATED) {
return;
}
synchronized (activeWritingContexts) {
if (activeWritingContexts.contains(contextId)) {
throw new EKBConcurrentException("There is already a writing process active in the context.");
}
activeWritingContexts.add(contextId);
}
}
/**
* Tests if the head revision for the given context matches the given revision number. If this is not the case, an
* EKBConcurrentException is thrown.
*/
private void checkForContextHeadRevision(String contextId, UUID expectedHeadRevision)
throws EKBConcurrentException {
if (!Objects.equal(edbService.getLastRevisionNumberOfContext(contextId), expectedHeadRevision)) {
throw new EKBConcurrentException("The current revision of the context does not match the "
+ "expected one.");
}
}
/**
* If the context locking mode is activated, this method releases the lock for the given context for writing
* operations.
*/
private void releaseContext(String contextId) {
if (mode == ContextLockingMode.DEACTIVATED) {
return;
}
synchronized (activeWritingContexts) {
activeWritingContexts.remove(contextId);
}
}
/**
* Runs all registered pre-commit hooks
*/
private void runEKBPreCommitHooks(EKBCommit commit) throws EKBException {
for (EKBPreCommitHook hook : preCommitHooks) {
try {
hook.onPreCommit(commit);
} catch (EKBException e) {
throw new EKBException("EDBException is thrown in a pre commit hook.", e);
} catch (Exception e) {
LOGGER.warn("An exception is thrown in a EKB pre commit hook.", e);
}
}
}
/**
* Runs all registered post-commit hooks
*/
private void runEKBPostCommitHooks(EKBCommit commit) throws EKBException {
for (EKBPostCommitHook hook : postCommitHooks) {
try {
hook.onPostCommit(commit);
} catch (Exception e) {
LOGGER.warn("An exception is thrown in a EKB post commit hook.", e);
}
}
}
/**
* Runs all registered error hooks
*/
private void runEKBErrorHooks(EKBCommit commit, EKBException exception) {
if (exception != null) {
for (EKBErrorHook hook : errorHooks) {
hook.onError(commit, exception);
}
throw exception;
}
}
/**
* Performs the sanity checks of the given models.
*/
private SanityCheckReport performSanityChecks(EKBCommit commit) throws SanityCheckException {
// TODO: [OPENENGSB-2717] implement sanity check logic
return null;
}
/**
* Performs the persisting of the models into the EDB.
*/
private void performPersisting(ConvertedCommit commit, EKBCommit source) throws EKBException {
try {
EDBCommit ci = edbService.createEDBCommit(commit.getInserts(), commit.getUpdates(), commit.getDeletes());
ci.setDomainId(source.getDomainId());
ci.setConnectorId(source.getConnectorId());
ci.setInstanceId(source.getInstanceId());
ci.setComment(source.getComment());
edbService.commit(ci);
source.setRevisionNumber(ci.getRevisionNumber());
source.setParentRevisionNumber(ci.getParentRevisionNumber());
} catch (EDBCheckException e) {
throw new ModelPersistException(convertEDBObjectList(e.getFailedInserts()),
convertEDBObjectList(e.getFailedUpdates()), e.getFailedDeletes(), e);
} catch (EDBException e) {
throw new EKBException("Error while commiting EKBCommit", e);
}
}
/**
* Converts a list of EDBObject instances into a list of corresponding model oids.
*/
private List<String> convertEDBObjectList(List<EDBObject> objects) {
List<String> oids = new ArrayList<>();
for (EDBObject object : objects) {
oids.add(object.getOID());
}
return oids;
}
@Override
public void deleteCommit(UUID revision, String contextId) throws EKBException {
if (revision == null || contextId == null) {
throw new EKBException("null revision or context not allowed");
}
try {
lockContext(contextId);
checkForContextHeadRevision(contextId, revision);
edbService.deleteCommit(revision);
} catch (EDBException e) {
throw new EKBException("Error reverting commit with revision " + revision, e);
} finally {
releaseContext(contextId);
}
}
}