/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* bstefanescu
*/
package org.nuxeo.runtime.reload;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.jar.Manifest;
import javax.transaction.Transaction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.Environment;
import org.nuxeo.common.utils.JarUtils;
import org.nuxeo.runtime.RuntimeService;
import org.nuxeo.runtime.RuntimeServiceException;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.deployment.preprocessor.DeploymentPreprocessor;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.DefaultComponent;
import org.nuxeo.runtime.services.event.Event;
import org.nuxeo.runtime.services.event.EventService;
import org.nuxeo.runtime.transaction.TransactionHelper;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.packageadmin.PackageAdmin;
/**
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*/
public class ReloadComponent extends DefaultComponent implements ReloadService {
private static final Log log = LogFactory.getLog(ReloadComponent.class);
protected static Bundle bundle;
protected Long lastFlushed;
public static BundleContext getBundleContext() {
return bundle.getBundleContext();
}
public static Bundle getBundle() {
return bundle;
}
@Override
public void activate(ComponentContext context) {
super.activate(context);
bundle = context.getRuntimeContext().getBundle();
}
@Override
public void deactivate(ComponentContext context) {
super.deactivate(context);
bundle = null;
}
@Override
public void reload() throws InterruptedException {
if (log.isDebugEnabled()) {
log.debug("Starting reload");
}
try {
reloadProperties();
} catch (IOException e) {
throw new RuntimeServiceException(e);
}
triggerReloadWithNewTransaction(RELOAD_EVENT_ID);
}
@Override
public void reloadProperties() throws IOException {
log.info("Reload runtime properties");
Framework.getRuntime().reloadProperties();
}
@Override
public void reloadRepository() throws InterruptedException {
log.info("Reload repository");
triggerReloadWithNewTransaction(RELOAD_REPOSITORIES_ID);
}
@Override
public void reloadSeamComponents() {
log.info("Reload Seam components");
Framework.getLocalService(EventService.class).sendEvent(
new Event(RELOAD_TOPIC, RELOAD_SEAM_EVENT_ID, this, null));
}
@Override
public void flush() {
log.info("Flush caches");
Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null));
flushJaasCache();
setFlushedNow();
}
@Override
public void flushJaasCache() {
log.info("Flush the JAAS cache");
Framework.getLocalService(EventService.class).sendEvent(
new Event("usermanager", "user_changed", this, "Deployer"));
setFlushedNow();
}
@Override
public void flushSeamComponents() {
log.info("Flush Seam components");
Framework.getLocalService(EventService.class).sendEvent(
new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null));
setFlushedNow();
}
@Override
public String deployBundle(File file) throws BundleException {
return deployBundle(file, false);
}
@Override
public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException {
String name = getOSGIBundleName(file);
if (name == null) {
log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath()));
return null;
}
String path = file.getAbsolutePath();
log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus()));
if (reloadResourceClasspath) {
URL url;
try {
url = new File(path).toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Framework.reloadResourceLoader(Collections.singletonList(url), null);
}
// check if this is a bundle first
Bundle newBundle = getBundleContext().installBundle(path);
if (newBundle == null) {
throw new IllegalArgumentException("Could not find a valid bundle at path: " + path);
}
Transaction tx = TransactionHelper.suspendTransaction();
try {
newBundle.start();
} finally {
TransactionHelper.resumeTransaction(tx);
}
log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(),
getRuntimeStatus()));
return newBundle.getSymbolicName();
}
@Override
public void undeployBundle(File file, boolean reloadResources) throws BundleException {
String name = getOSGIBundleName(file);
String path = file.getAbsolutePath();
if (name == null) {
log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path));
return;
}
undeployBundle(name);
if (reloadResources) {
URL url;
try {
url = new File(path).toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Framework.reloadResourceLoader(null, Collections.singletonList(url));
}
}
@Override
public void undeployBundle(String bundleName) throws BundleException {
if (bundleName == null) {
// ignore
return;
}
log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus()));
BundleContext ctx = getBundleContext();
ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName());
PackageAdmin srv = (PackageAdmin) ctx.getService(ref);
try {
for (Bundle b : srv.getBundles(bundleName, null)) {
if (b != null && b.getState() == Bundle.ACTIVE) {
Transaction tx = TransactionHelper.suspendTransaction();
try {
b.stop();
b.uninstall();
} finally {
TransactionHelper.resumeTransaction(tx);
}
}
}
} finally {
ctx.ungetService(ref);
}
log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus()));
}
@Override
public Long lastFlushed() {
return lastFlushed;
}
/**
* Sets the last date date to current date timestamp
*
* @since 5.6
*/
protected void setFlushedNow() {
lastFlushed = System.currentTimeMillis();
}
@Override
public void runDeploymentPreprocessor() throws IOException {
if (log.isDebugEnabled()) {
log.debug("Start running deployment preprocessor");
}
String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath();
File root = new File(rootPath);
DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
// initialize
processor.init();
// and predeploy
processor.predeploy();
if (log.isDebugEnabled()) {
log.debug("Deployment preprocessing done");
}
}
protected static File getAppDir() {
return Environment.getDefault().getConfig().getParentFile();
}
protected static File getWarDir() {
return new File(getAppDir(), "nuxeo.war");
}
@Override
public String getOSGIBundleName(File file) {
Manifest mf = JarUtils.getManifest(file);
if (mf == null) {
return null;
}
String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName");
if (bundleName == null) {
return null;
}
int index = bundleName.indexOf(';');
if (index > -1) {
bundleName = bundleName.substring(0, index);
}
return bundleName;
}
protected String getRuntimeStatus() {
StringBuilder msg = new StringBuilder();
RuntimeService runtime = Framework.getRuntime();
runtime.getStatusMessage(msg);
return msg.toString();
}
protected void triggerReloadWithNewTransaction(String id) throws InterruptedException {
if (TransactionHelper.isTransactionMarkedRollback()) {
throw new AssertionError("The calling transaction is marked rollback");
}
// we need to commit or rollback transaction because suspending it leads to a lock/errors when acquiring a new
// connection during the datasource reload
TransactionHelper.commitOrRollbackTransaction();
TransactionHelper.startTransaction();
try {
try {
triggerReload(id);
} catch (RuntimeException cause) {
TransactionHelper.setTransactionRollbackOnly();
throw cause;
} finally {
TransactionHelper.commitOrRollbackTransaction();
}
} finally {
TransactionHelper.startTransaction();
}
}
protected void triggerReload(String id) throws InterruptedException {
log.info("about to reload for " + id);
Framework.getLocalService(EventService.class).sendEvent(
new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null));
Framework.getRuntime().standby(Instant.now().plus(Duration.ofSeconds(30)));
try {
Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null));
Framework.getRuntime().resume();
} finally {
Framework.getLocalService(EventService.class).sendEvent(
new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null));
log.info("returning from " + id);
}
}
}