/**
* 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.services.internal.deployer.connector;
import java.io.File;
import java.io.FileWriter;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.fileinstall.ArtifactInstaller;
import org.openengsb.core.api.ConnectorInstanceFactory;
import org.openengsb.core.api.ConnectorManager;
import org.openengsb.core.api.DomainProvider;
import org.openengsb.core.api.model.ConnectorDescription;
import org.openengsb.core.common.AbstractOpenEngSBService;
import org.openengsb.core.services.SecurityContext;
import org.openengsb.core.services.internal.deployer.connector.ConnectorFile.ChangeSet;
import org.openengsb.core.util.ConfigUtils;
import org.openengsb.core.util.MergeException;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Sets;
public class ConnectorDeployerService extends AbstractOpenEngSBService implements ArtifactInstaller {
private static final String CONNECTOR_EXTENSION = ".connector";
private static final String DOMAIN_PATTERN = "(" + org.openengsb.core.api.Constants.DOMAIN_KEY + "=%s)";
private static final String DOMAIN_CONNECTOR_PATTERN = "(" + "&(" + org.openengsb.core.api.Constants.DOMAIN_KEY
+ "=%s)" + "(" + org.openengsb.core.api.Constants.CONNECTOR_KEY + "=%s)" + ")";
private static final String DOMAIN_PROVIDER_PATTERN = "(" + Constants.OBJECTCLASS + "="
+ DomainProvider.class.getName() + ")";
private static final String CONNECTOR_FACTORY_PATTERN = "(" + Constants.OBJECTCLASS + "="
+ ConnectorInstanceFactory.class.getName() + ")";
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectorDeployerService.class);
private ConnectorManager serviceManager;
private final LoadingCache<File, ConnectorFile> oldConfigs = CacheBuilder.newBuilder().build(
new CacheLoader<File, ConnectorFile>() {
@Override
public ConnectorFile load(File key) throws Exception {
return new ConnectorFile(key);
}
});
private final LoadingCache<File, Semaphore> updateSemaphores = CacheBuilder.newBuilder().build(
new CacheLoader<File, Semaphore>() {
@Override
public Semaphore load(File key) throws Exception {
return new Semaphore(1);
}
});
private final Set<File> failedInstalls = Sets.newHashSet();
private BundleContext bundleContext;
public void init() throws InvalidSyntaxException {
bundleContext.addServiceListener(new ServiceListener() {
@Override
public void serviceChanged(ServiceEvent event) {
try {
tryInstallFailed();
} catch (Exception e) {
LOGGER.debug("exception while trying to install connectors after new found domain or connector",
e);
}
}
}, "(|" + DOMAIN_PROVIDER_PATTERN + CONNECTOR_FACTORY_PATTERN + ")");
}
@Override
public boolean canHandle(File artifact) {
LOGGER.debug("ConnectorDeployer.canHandle(\"{}\")", artifact.getAbsolutePath());
if (artifact.isFile() && artifact.getName().endsWith(CONNECTOR_EXTENSION)) {
LOGGER.info("Found a .connector file to deploy.");
return true;
}
return false;
}
@Override
public void install(File artifact) throws Exception {
LOGGER.debug("ConnectorDeployer.install(\"{}\")", artifact.getAbsolutePath());
synchronized (failedInstalls) {
if (!doInstall(artifact)) {
failedInstalls.add(artifact);
}
}
}
private boolean doInstall(File artifact) {
final ConnectorFile configFile;
try {
configFile = oldConfigs.get(artifact);
} catch (ExecutionException e) {
LOGGER.error("severe error when installing artifact", e);
return false;
}
configFile.update(artifact);
final Map<String, Object> properties = new Hashtable<String, Object>(configFile.getProperties());
if (properties.get(Constants.SERVICE_RANKING) == null && ConnectorFile.isRootService(artifact)) {
properties.put(Constants.SERVICE_RANKING, -1);
}
LOGGER.info("Loading instance {}", configFile.getName());
final String name = FilenameUtils.removeExtension(artifact.getName());
final Map<String, String> attributes = configFile.getAttributes();
if (!haveDomainProvider(configFile.getDomainType())) {
LOGGER.info("installing {} delayed. Waiting for DomainProvider", artifact);
return false;
}
if (!haveConnectorFactory(configFile.getDomainType(), configFile.getConnectorType())) {
LOGGER.info("installing {} delayed. Waiting for ConnectorFactory", artifact);
return false;
}
try {
SecurityContext.executeWithSystemPermissions(new Callable<Object>() {
@Override
public Object call() throws Exception {
ConnectorDescription connectorDescription =
new ConnectorDescription(configFile.getDomainType(), configFile.getConnectorType(),
attributes, properties);
try {
if (serviceManager.connectorExists(name)) {
serviceManager.update(name, connectorDescription);
} else {
serviceManager.createWithId(name, connectorDescription);
}
} catch (IllegalArgumentException e) {
throw e;
}
return null;
}
});
} catch (org.apache.shiro.subject.ExecutionException e) {
LOGGER.info("installing {} delayed", artifact);
LOGGER.debug("installing {} failed because of Exception", artifact, e);
return false;
}
LOGGER.info("Finished installing {}", artifact);
return true;
}
public void tryInstallFailed() {
synchronized (failedInstalls) {
Iterator<File> failedInstallsIterator = failedInstalls.iterator();
while (failedInstallsIterator.hasNext()) {
File fileToInstall = failedInstallsIterator.next();
if (doInstall(fileToInstall)) {
failedInstallsIterator.remove();
}
}
}
}
@Override
public void update(File artifact) throws Exception {
LOGGER.debug("ConnectorDeployer.update(\"{}\")", artifact.getAbsolutePath());
Semaphore semaphore = updateSemaphores.get(artifact);
semaphore.acquire();
try {
doUpdate(artifact);
} finally {
semaphore.release();
}
}
private void doUpdate(File artifact) throws Exception {
ConnectorFile connectorFile = oldConfigs.get(artifact);
final String connectorId = connectorFile.getName();
ConnectorDescription persistenceContent = serviceManager.getAttributeValues(connectorId);
ChangeSet changes = connectorFile.getChanges(artifact);
final ConnectorDescription newDescription;
try {
newDescription = applyChanges(persistenceContent, changes);
connectorFile.update(artifact);
} catch (MergeException e) {
File backupFile = getBackupFile(artifact);
FileUtils.moveFile(artifact, backupFile);
Properties properties = connectorFile.toProperties();
properties.store(new FileWriter(artifact),
"Connector update failed. The invalid connector-file has been saved to " + backupFile.getName());
throw e;
}
SecurityContext.executeWithSystemPermissions(new Callable<Object>() {
@Override
public Object call() throws Exception {
serviceManager.update(connectorId, newDescription);
return null;
}
});
}
private File getBackupFile(File artifact) {
int backupNumber = 0;
String candidate = artifact.getAbsolutePath();
File candFile = new File(candidate);
while (candFile.exists()) {
backupNumber++;
String suffix = StringUtils.leftPad(Integer.toString(backupNumber), 3, "0");
candidate = artifact.getAbsolutePath() + "_" + suffix;
candFile = new File(candidate);
}
return candFile;
}
private ConnectorDescription applyChanges(ConnectorDescription persistenceContent, ChangeSet changes)
throws MergeException {
MapDifference<String, String> changedAttributes = changes.getChangedAttributes();
Map<String, String> attributes = persistenceContent.getAttributes();
Map<String, String> newAttributes = ConfigUtils.updateMap(attributes, changedAttributes);
Map<String, Object> newProperties =
ConfigUtils.updateMap(persistenceContent.getProperties(), changes.getChangedProperties());
return new ConnectorDescription(changes.getDomainType(), changes.getConnectorType(), newAttributes,
new Hashtable<String, Object>(newProperties));
}
@Override
public void uninstall(final File artifact) throws Exception {
LOGGER.debug("ConnectorDeployer.uninstall(\"{}\")", artifact.getAbsolutePath());
SecurityContext.executeWithSystemPermissions(new Callable<Object>() {
@Override
public Object call() throws Exception {
String name = FilenameUtils.removeExtension(artifact.getName());
serviceManager.delete(name);
return null;
}
});
}
protected boolean haveConnectorFactory(String domainType, String connectorType) {
String connectorFilter = String.format(DOMAIN_CONNECTOR_PATTERN, domainType, connectorType);
try {
Collection<ServiceReference<ConnectorInstanceFactory>> serviceReferences =
bundleContext.getServiceReferences(ConnectorInstanceFactory.class, connectorFilter);
return !serviceReferences.isEmpty();
} catch (InvalidSyntaxException e) {
throw new RuntimeException(e);
}
}
private boolean haveDomainProvider(String domain) {
String domainFilter = String.format(DOMAIN_PATTERN, domain);
try {
Collection<ServiceReference<DomainProvider>> serviceReferences =
bundleContext.getServiceReferences(DomainProvider.class, domainFilter);
return !serviceReferences.isEmpty();
} catch (InvalidSyntaxException e) {
throw new RuntimeException(e);
}
}
public void setServiceManager(ConnectorManager serviceManager) {
this.serviceManager = serviceManager;
}
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
}