/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015 ForgeRock AS.
*/
package org.forgerock.openidm.maintenance.upgrade;
import static org.forgerock.json.JsonValue.array;
import static org.forgerock.json.JsonValue.field;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Attributes;
import java.util.regex.Pattern;
import difflib.DiffUtils;
import difflib.Patch;
import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import org.apache.commons.io.FileUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.forgerock.commons.launcher.OSGiFrameworkService;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.SortKey;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.router.IDMConnectionFactory;
import org.forgerock.openidm.util.ContextUtil;
import org.forgerock.openidm.util.FileUtil;
import org.forgerock.services.context.Context;
import org.forgerock.util.Function;
import org.forgerock.util.query.QueryFilter;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.service.component.ComponentContext;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Basic manager to initiate the product maintenance and upgrade mechanisms.
*/
@Component(name = UpdateManagerImpl.PID, policy = ConfigurationPolicy.IGNORE, metatype = true,
description = "OpenIDM Update Manager", immediate = true)
@Service
@org.apache.felix.scr.annotations.Properties({
@Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME),
@Property(name = Constants.SERVICE_DESCRIPTION, value = "Product Update Manager")
})
public class UpdateManagerImpl implements UpdateManager {
/** The PID for this component. */
public static final String PID = "org.forgerock.openidm.maintenance.updatemanager";
private final static Logger logger = LoggerFactory.getLogger(UpdateManagerImpl.class);
private static final Path CHECKSUMS_FILE = Paths.get(".checksums.csv");
private static final Path BUNDLE_PATH = Paths.get("bundle");
private static final Path CONF_PATH = Paths.get("conf");
private static final String JSON_EXT = ".json";
private static final String PATCH_EXT = ".patch";
private static final Path ARCHIVE_PATH = Paths.get("bin/update");
private static final String LICENSE_PATH = "legal-notices/license.txt";
// Archive manifest keys
private static final String PROP_PRODUCT = "product";
private static final String PROP_VERSION = "version";
private static final String PROP_UPGRADESPRODUCT = "upgradesProduct";
private static final String PROP_UPGRADESVERSION = "upgradesVersion";
private static final String PROP_DESCRIPTION = "description";
private static final String PROP_RESOURCE = "resource";
private static final String PROP_RESTARTREQUIRED = "restartRequired";
private static final String BUNDLE_BACKUP_EXT = ".old-";
protected final AtomicBoolean restartImmediately = new AtomicBoolean(false);
private UpdateThread updateThread = null;
private String lastUpdateId = null;
public enum UpdateStatus {
IN_PROGRESS,
COMPLETE,
FAILED
}
/** The OSGiFramework Service **/
protected OSGiFrameworkService osgiFrameworkService;
@Activate
void activate(ComponentContext compContext) throws Exception {
logger.debug("Activating UpdateManagerImpl {}", compContext.getProperties());
BundleContext bundleContext = compContext.getBundleContext();
Filter filter = bundleContext
.createFilter("(" + Constants.OBJECTCLASS + "=org.forgerock.commons.launcher.OSGiFramework)");
ServiceTracker serviceTracker = new ServiceTracker(bundleContext, filter, null);
serviceTracker.open(true);
this.osgiFrameworkService = (OSGiFrameworkService) serviceTracker.getService();
if (osgiFrameworkService != null) {
logger.debug("Obtained OSGiFrameworkService", compContext.getProperties());
} else {
throw new InternalServerErrorException("Cannot instantiate service without OSGiFrameworkService");
}
}
/** The update logging service */
@Reference(policy = ReferencePolicy.STATIC)
private UpdateLogService updateLogService;
/** The connection factory */
@Reference(policy = ReferencePolicy.STATIC)
protected IDMConnectionFactory connectionFactory;
/**
* Execute a {@link Function} on an input stream for the given {@link Path}.
*
* @param path the {@link Path} on which to open an {@link InputStream}
* @param function the {@link Function} to be applied to that {@link InputStream}
* @param <R> The return type of the function
* @param <E> The exception type thrown by the function
* @return The result of the function
* @throws E on exception from the function
* @throws IOException on failure to create an input stream from the path given
*/
static <R, E extends Exception> R withInputStreamForPath(Path path, Function<InputStream, R, E> function)
throws E, IOException {
try (final InputStream is = Files.newInputStream(path.normalize())) {
return function.apply(is);
}
}
private class ZipArchive implements Archive {
private final Path upgradeRoot;
private final Set<Path> filePaths;
private ProductVersion version = null;
ZipArchive(Path zipFilePath, Path destination) throws ArchiveException {
upgradeRoot = destination.resolve("openidm");
// unzip the upgrade dist
try {
ZipFile zipFile = new ZipFile(zipFilePath.toString());
if (zipFile.isEncrypted()) {
throw new UnsupportedOperationException("Encrypted zip files are not supported");
}
zipFile.extractAll(destination.toString());
} catch (ZipException e) {
throw new ArchiveException("Can't read archive file: " + zipFilePath.toAbsolutePath(), e);
}
// get the file set from the upgrade dist checksum file
try {
filePaths = new ChecksumFile(upgradeRoot.resolve(CHECKSUMS_FILE)).getFilePaths();
} catch (Exception e) {
throw new ArchiveException("Archive doesn't appear to contain checksums file - invalid archive?", e);
}
// get the version from the embedded ServerConstants
try {
final URL targetUrl = upgradeRoot
.resolve(BUNDLE_PATH)
.resolve(zipFilePath
.getFileName()
.toString()
.replaceAll("^openidm-(.*).zip$", "openidm-system-$1.jar"))
.toFile()
.toURI()
.toURL();
try (final URLClassLoader loader = new URLClassLoader(new URL[] { targetUrl })) {
final Class<?> c = loader.loadClass("org.forgerock.openidm.core.ServerConstants");
final Method getVersion = c.getMethod("getVersion");
final Method getRevision = c.getMethod("getRevision");
version = new ProductVersion(
String.valueOf(getVersion.invoke(null)),
String.valueOf(getRevision.invoke(null)));
logger.info("Upgrading to " + version);
}
} catch (IOException
| ClassNotFoundException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException e) {
logger.info("Archive does not contain a product version; Assumed to be a patch.");
}
}
@Override
public ProductVersion getVersion() {
return version;
}
@Override
public Set<Path> getFiles() {
return filePaths;
}
@Override
public <R, E extends Exception> R withInputStreamForPath(Path path, Function<InputStream, R, E> function)
throws E, IOException {
return UpdateManagerImpl.withInputStreamForPath(upgradeRoot.resolve(path), function);
}
}
/**
* Execute a {@link Function} in a temp directory prefixed by {@code tempDirectoryPrefix}. This function
* is useful to do a body of work in a temp directory and then clean up the contents of the temp directory
* and remove the temp directory once the work is complete.
*
* @param tempDirectoryPrefix the temp directory prefix
* @param func a {@link Function} to execute in/on a temp directory
* @param <R> The return type of the function
* @return the result of the function
* @throws UpdateException on failure to create the temp directory or execute the function
*/
private <R> R withTempDirectory(String tempDirectoryPrefix, Function<Path, R, UpdateException> func)
throws UpdateException {
Path tempUnzipDir = null;
try {
tempUnzipDir = Files.createTempDirectory(tempDirectoryPrefix);
return func.apply(tempUnzipDir);
} catch (IOException e) {
throw new UpdateException("Cannot create temporary directory to unzip archive");
} finally {
try {
if (tempUnzipDir != null) {
FileUtils.deleteDirectory(tempUnzipDir.toFile());
}
} catch (IOException e) {
logger.error("Could not remove temporary directory: " + tempUnzipDir.toString(), e);
}
}
}
/**
* An interface for upgrade actions.
*
* @param <R> the return type (often JsonValue)
*/
private interface UpgradeAction<R> {
R invoke(Archive archive, FileStateChecker fileStateChecker) throws UpdateException;
}
/**
* Invoke an {@link UpgradeAction} using an archive at a given URL. Use {@code installDir} as the location
* of the currently-installed OpenIDM.
*
* @param archiveFile the {@link Path} to a ZIP archive containing a new version of OpenIDM
* @param installDir the {@link Path} of the currently-installed OpenIDM
* @param upgradeAction the upgrade action to perform
* @param <R> The return type of the action
* @return the result of the upgrade action
* @throws UpdateException on failure to perform the upgrade action
*/
private <R> R usingArchive(final Path archiveFile, final Path installDir, final UpgradeAction<R> upgradeAction)
throws UpdateException {
return withTempDirectory("openidm-upgrade-",
new Function<Path, R, UpdateException>() {
@Override
public R apply(Path tempUnzipDir) throws UpdateException {
try {
final ZipArchive archive = new ZipArchive(archiveFile, tempUnzipDir);
final ChecksumFile checksumFile = new ChecksumFile(installDir.resolve(CHECKSUMS_FILE));
final FileStateChecker fileStateChecker = new FileStateChecker(checksumFile);
return upgradeAction.invoke(archive, fileStateChecker);
} catch (Exception e) {
throw new UpdateException(e.getMessage(), e);
}
}
});
}
/**
* {@inheritDoc}
*/
@Override
public JsonValue listAvailableUpdates() throws UpdateException {
final JsonValue updates = json(array());
final ChecksumFile checksumFile;
try {
checksumFile = new ChecksumFile(Paths.get(".").resolve(CHECKSUMS_FILE));
} catch (IOException | NoSuchAlgorithmException e) {
throw new UpdateException("Failed to load checksum file from archive.", e);
} catch (NullPointerException e) {
throw new UpdateException("Archive directory does not exist?", e);
}
for (final File file : ARCHIVE_PATH.toFile().listFiles()) {
if (file.getName().endsWith(".zip")) {
try {
Properties prop = readProperties(file);
if ("OpenIDM".equals(prop.getProperty(PROP_UPGRADESPRODUCT)) &&
ServerConstants.getVersion().equals(prop.getProperty(PROP_UPGRADESVERSION))) {
updates.add(object(
field("archive", file.getName()),
field("fileSize", file.length()),
field("fileDate", file.lastModified()),
field("checksum", checksumFile.getCurrentDigest(file.toPath())),
field("version", prop.getProperty(PROP_PRODUCT) + " v" +
prop.getProperty(PROP_VERSION)),
field("description", prop.getProperty(PROP_DESCRIPTION)),
field("resource", prop.getProperty(PROP_RESOURCE)),
field("restartRequired", prop.getProperty(PROP_RESTARTREQUIRED))
));
}
} catch (Exception e) {
// skip file, it does not contain a manifest or digest could not be calculated
}
}
}
return updates;
}
/**
* {@inheritDoc}
*/
@Override
public JsonValue report(final Path archiveFile, final Path installDir)
throws UpdateException {
return usingArchive(archiveFile, installDir,
new UpgradeAction<JsonValue>() {
@Override
public JsonValue invoke(Archive archive, FileStateChecker fileStateChecker)
throws UpdateException {
final List<Object> result = array();
for (Path path : archive.getFiles()) {
try {
FileState state = fileStateChecker.getCurrentFileState(path);
result.add(object(
field("filePath", path.toString()),
field("fileState", state.toString())
));
} catch (IOException e) {
throw new UpdateException("Unable to determine file state for " + path.toString(), e);
}
}
return json(result);
}
});
}
/**
* {@inheritDoc}
*/
@Override
public JsonValue diff(final Path archiveFile, final Path installDir, final String filename) throws UpdateException {
return usingArchive(archiveFile, installDir,
new UpgradeAction<JsonValue>() {
// Helper function for get the file content
private Function<InputStream, List<String>, IOException> inputStreamToLines =
new Function<InputStream, List<String>, IOException>() {
@Override
public List<String> apply(InputStream is) throws IOException {
final List<String> lines = new LinkedList<String>();
String line = "";
try (final InputStreamReader isr = new InputStreamReader(is);
final BufferedReader in = new BufferedReader(isr)) {
while ((line = in.readLine()) != null) {
lines.add(line);
}
}
return lines;
}
};
@Override
public JsonValue invoke(Archive archive, FileStateChecker fileStateChecker) throws UpdateException {
final Path file = Paths.get(filename);
try {
final List<String> currentFileLines = withInputStreamForPath(installDir.resolve(file), inputStreamToLines);
final List<String> newFileLines = archive.withInputStreamForPath(file, inputStreamToLines);
Patch<String> patch = DiffUtils.diff(currentFileLines, newFileLines);
return json(object(
field("current", currentFileLines),
field("new", newFileLines),
field("diff", DiffUtils.generateUnifiedDiff(
Paths.get("current").resolve(file).toString(),
Paths.get("new").resolve(file).toString(),
currentFileLines,
patch,
3))));
} catch (IOException e) {
throw new UpdateException("Unable to retrieve file content for " + file.toString(), e);
}
}
});
}
/**
* {@inheritDoc}
*/
@Override
public JsonValue upgrade(final Path archiveFile, final Path installDir, final String userName)
throws UpdateException {
final Properties prop = readProperties(archiveFile.toFile());
if (!"OpenIDM".equals(prop.getProperty(PROP_UPGRADESPRODUCT)) ||
!ServerConstants.getVersion().equals(prop.getProperty(PROP_UPGRADESVERSION))) {
throw new UpdateException("Update archive does not apply to the installed product.");
}
Path tempUnzipDir = null;
try {
tempUnzipDir = Files.createTempDirectory("openidm-upgrade-");
try {
final ZipArchive archive = new ZipArchive(archiveFile, tempUnzipDir);
final ChecksumFile checksumFile = new ChecksumFile(installDir.resolve(CHECKSUMS_FILE));
final FileStateChecker fileStateChecker = new FileStateChecker(checksumFile);
// perform upgrade
UpdateLogEntry updateEntry = new UpdateLogEntry();
updateEntry.setStatus(UpdateStatus.IN_PROGRESS)
.setStatusMessage("Initializing update")
.setTotalTasks(archive.getFiles().size())
.setStartDate(getDateString())
.setNodeId(IdentityServer.getInstance().getNodeName())
.setUserName(userName);
try {
updateLogService.logUpdate(updateEntry);
} catch (ResourceException e) {
throw new UpdateException("Unable to log update.", e);
}
updateThread = new UpdateThread(updateEntry, archive, fileStateChecker, installDir, prop,
tempUnzipDir);
updateThread.start();
return updateEntry.toJson();
} catch (Exception e) {
throw new UpdateException(e.getMessage(), e);
}
} catch (IOException e) {
throw new UpdateException("Cannot create temporary directory to unzip archive");
}
}
/**
* {@inheritDoc}
*/
@Override
public JsonValue getLicense(Path archive) throws UpdateException {
try {
ZipFile zip = new ZipFile(archive.toFile());
Path tmpDir = Files.createTempDirectory(UUID.randomUUID().toString());
zip.extractFile("openidm/" + LICENSE_PATH, tmpDir.toString());
File file = new File(tmpDir.toString() + "/openidm/" + LICENSE_PATH);
if (!file.exists()) {
throw new UpdateException("Unable to locate a license file.");
}
try (FileInputStream inp = new FileInputStream(file)) {
byte[] data = new byte[(int) file.length()];
inp.read(data);
return json(object(field("license", new String(data, "UTF-8"))));
} catch (IOException e) {
throw new UpdateException("Unable to load license file.", e);
}
} catch (IOException | ZipException e) {
return json(object());
}
}
/**
* {@inheritDoc}
*/
@Override
public void restartNow() {
restartImmediately.set(true);
if (updateThread == null) {
try {
new UpdateThread().restart();
} catch (BundleException e) {
logger.debug("Failed to restart!", e);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public String getLastUpdateId() {
if (lastUpdateId == null) {
final List<JsonValue> results = new ArrayList<>();
final QueryRequest request = Requests.newQueryRequest("repo/updates")
.addField("_id")
.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue())
.addSortKey(SortKey.descendingOrder("startDate"))
.setPageSize(1);
try {
connectionFactory.getConnection().query(ContextUtil.createInternalContext(), request,
new QueryResourceHandler() {
@Override
public boolean handleResource(ResourceResponse resourceResponse) {
results.add(resourceResponse.getContent());
return true;
}
});
} catch (ResourceException e) {
logger.debug("Unable to retrieve most recent update from repo", e);
return "0";
}
lastUpdateId = results.size() > 0 ? results.get(0).get(ResourceResponse.FIELD_CONTENT_ID).asString() : "0";
}
return lastUpdateId;
}
/**
* Actions taken during the update of a file
*/
private enum UpdateAction {
REPLACED,
PRESERVED,
APPLIED
}
private class UpdateThread extends Thread {
private final UpdateLogEntry updateEntry;
private final Archive archive;
private final FileStateChecker fileStateChecker;
private final StaticFileUpdate staticFileUpdate;
private final Properties updateProperties;
private final Path tempDirectory;
private final long timestamp = new Date().getTime();
public UpdateThread() {
this.updateEntry = null;
this.archive = null;
this.fileStateChecker = null;
this.staticFileUpdate = null;
this.updateProperties = null;
this.tempDirectory = null;
}
public UpdateThread(UpdateLogEntry updateEntry, Archive archive, FileStateChecker fileStateChecker,
Path installDir, Properties updateProperties, Path tempDirectory) {
this.updateEntry = updateEntry;
this.archive = archive;
this.fileStateChecker = fileStateChecker;
this.updateProperties = updateProperties;
this.tempDirectory = tempDirectory;
this.staticFileUpdate = new StaticFileUpdate(fileStateChecker, installDir,
archive, new ProductVersion(ServerConstants.getVersion(),
ServerConstants.getRevision()), timestamp);
}
public void run() {
if (updateEntry == null) {
return;
}
try {
String projectDir = IdentityServer.getInstance().getProjectLocation().toString();
final String installDir = IdentityServer.getInstance().getInstallLocation().toString();
BundleHandler bundleHandler = new BundleHandler(
osgiFrameworkService.getSystemBundle().getBundleContext(), BUNDLE_BACKUP_EXT + timestamp,
new LogHandler() {
@Override
public void log(Path filePath, Path backupPath) {
try {
Path newPath = Paths.get(filePath.toString().substring(
tempDirectory.toString().length() + "/openidm/".length()));
UpdateFileLogEntry fileEntry = new UpdateFileLogEntry()
.setFilePath(newPath.toString())
.setFileState(fileStateChecker.getCurrentFileState(newPath).name())
.setActionTaken(UpdateAction.REPLACED.toString());
fileEntry.setBackupFile(backupPath.toString().substring(installDir.length() + 1));
logUpdate(updateEntry.addFile(fileEntry.toJson()));
} catch (Exception e) {
logger.debug("Failed to log updated file: " + filePath.toString());
}
}
});
for (final Path path : archive.getFiles()) {
if (path.startsWith(BUNDLE_PATH)) {
Path newPath = Paths.get(tempDirectory.toString(), "openidm", path.toString());
String symbolicName = null;
try {
Attributes manifest = readManifest(newPath);
symbolicName = manifest.getValue(Constants.BUNDLE_SYMBOLICNAME);
} catch (Exception e) {
// jar does not contain a manifest
}
if (symbolicName == null) {
// treat it as a static file
Path backupFile = staticFileUpdate.replace(path);
if (backupFile != null) {
UpdateFileLogEntry fileEntry = new UpdateFileLogEntry()
.setFilePath(path.toString())
.setFileState(fileStateChecker.getCurrentFileState(path).name())
.setActionTaken(UpdateAction.REPLACED.toString());
fileEntry.setBackupFile(backupFile.toString());
logUpdate(updateEntry.addFile(fileEntry.toJson()));
}
} else {
bundleHandler.upgradeBundle(newPath, symbolicName);
fileStateChecker.updateState(path);
}
} else if (path.getFileName().toString().endsWith(JSON_EXT) &&
!projectDir.equals(installDir) &&
path.startsWith(projectDir.substring(installDir.length() + 1) + "/" + CONF_PATH)) {
// a json config in the current project - ignore it
} else if (path.startsWith(CONF_PATH) &&
path.getFileName().toString().endsWith(JSON_EXT)) {
// a json config in the default project - ignore it
} else if (path.startsWith(CONF_PATH) &&
path.getFileName().toString().endsWith(PATCH_EXT)) {
// a patch file for a config in the repo
patchConfig(ContextUtil.createInternalContext(),
"repo/config", json(FileUtil.readFile(path.toFile())));
UpdateFileLogEntry fileEntry = new UpdateFileLogEntry()
.setFilePath(path.toString())
.setFileState(fileStateChecker.getCurrentFileState(path).name())
.setActionTaken(UpdateAction.APPLIED.toString());
logUpdate(updateEntry.addFile(fileEntry.toJson()));
} else {
// normal static file; update it
UpdateFileLogEntry fileEntry = new UpdateFileLogEntry()
.setFilePath(path.toString())
.setFileState(fileStateChecker.getCurrentFileState(path).name());
if (!isReadOnly(path)) {
Path stockFile = staticFileUpdate.keep(path);
fileEntry.setActionTaken(UpdateAction.PRESERVED.toString());
if (stockFile != null) {
fileEntry.setStockFile(stockFile.toString());
}
} else {
Path backupFile = staticFileUpdate.replace(path);
fileEntry.setActionTaken(UpdateAction.REPLACED.toString());
if (backupFile != null) {
fileEntry.setBackupFile(backupFile.toString());
}
}
if (fileEntry.getStockFile() != null || fileEntry.getBackupFile() != null) {
logUpdate(updateEntry.addFile(fileEntry.toJson()));
}
}
logUpdate(updateEntry.setCompletedTasks(updateEntry.getCompletedTasks() + 1)
.setStatusMessage("Processed " + path.getFileName().toString()));
}
logUpdate(updateEntry.setEndDate(getDateString())
.setStatus(UpdateStatus.COMPLETE)
.setStatusMessage("Update complete."));
} catch (Exception e) {
try {
logUpdate(updateEntry.setEndDate(getDateString())
.setStatus(UpdateStatus.FAILED)
.setStatusMessage("Update failed."));
} catch (UpdateException ue) {}
logger.debug("Failed to install update!", e);
return;
} finally {
try {
if (tempDirectory != null) {
FileUtils.deleteDirectory(tempDirectory.toFile());
}
} catch (IOException e) {
logger.error("Could not remove temporary directory: " + tempDirectory.toString(), e);
}
}
if (Boolean.valueOf(updateProperties.getProperty(PROP_RESTARTREQUIRED).toUpperCase())) {
try {
restart();
} catch (BundleException e) {
logger.debug("Failed to restart!", e);
}
}
}
protected void restart() throws BundleException {
long timeout = System.currentTimeMillis() + 30000;
try {
do {
sleep(200);
} while (System.currentTimeMillis() < timeout && !restartImmediately.get());
} catch (Exception e) {
// restart now
}
// Send updated FrameworkEvent
osgiFrameworkService.getSystemBundle().update();
}
/**
* Apply a JsonPatch to a config object on the router.
*
* @param context the context for the patch request.
* @param resourceName the name of the resource to be patched.
* @param patch a JsonPatch to be applied to the named config resource.
* @throws UpdateException
*/
private void patchConfig(Context context, String resourceName, JsonValue patch) throws UpdateException {
try {
PatchRequest request = Requests.newPatchRequest(resourceName);
for (PatchOperation op : PatchOperation.valueOfList(patch)) {
request.addPatchOperation(op);
}
UpdateManagerImpl.this.connectionFactory.getConnection().patch(context, request);
} catch (ResourceException e) {
throw new UpdateException("Patch request failed", e);
}
}
}
private boolean isReadOnly(Path path) {
Pattern uiDefaults = Pattern.compile("^ui/*/default");
return path.startsWith("bin") || uiDefaults.matcher(path.toString()).find();
}
private void logUpdate(UpdateLogEntry entry) throws UpdateException {
try {
updateLogService.updateUpdate(entry);
} catch (ResourceException e) {
throw new UpdateException("Failed to modify update log entry.", e);
}
}
private String getDateString() {
// Ex: 2011-09-09T14:58:17.654+02:00
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SXXX");
return formatter.format(new Date());
}
private Properties readProperties(File file) throws UpdateException {
Properties prop = new Properties();
try {
ZipFile zip = new ZipFile(file);
Path tmpDir = Files.createTempDirectory(UUID.randomUUID().toString());
zip.extractFile("openidm/package.properties", tmpDir.toString());
try (InputStream inp = new FileInputStream(tmpDir.toString() + "/openidm/package.properties")) {
prop.load(inp);
return prop;
} catch (IOException e) {
throw new UpdateException("Unable to load package properties.", e);
} finally {
new File(tmpDir.toString() + "/openidm/package.properties").delete();
}
} catch (IOException | ZipException e) {
throw new UpdateException("Unable to load package properties.", e);
}
}
private Attributes readManifest(Path jarFile) throws UpdateException {
try {
return FileUtil.readManifest(jarFile.toFile());
} catch (FileNotFoundException e) {
throw new UpdateException("File " + jarFile.toFile().getName() + " does not exist.", e);
} catch (Exception e) {
throw new UpdateException("Error while reading from " + jarFile.toFile().getName(), e);
}
}
/**
* Fetch the zip file from the URL and write it to the local filesystem.
*
* @param url
* @return
* @throws UpdateException
*/
private Path readZipFile(URL url) throws UpdateException {
// Download the patch file
final ReadableByteChannel channel;
try {
channel = Channels.newChannel(url.openStream());
} catch (IOException ex) {
throw new UpdateException("Failed to access the specified file " + url + " " + ex.getMessage(), ex);
}
String workingDir = "";
final String targetFileName = new File(url.getPath()).getName();
final File patchDir = ARCHIVE_PATH.toFile();
patchDir.mkdirs();
final File targetFile = new File(patchDir, targetFileName);
final FileOutputStream fos;
try {
fos = new FileOutputStream(targetFile);
} catch (FileNotFoundException ex) {
throw new UpdateException("Error in getting the specified file to " + targetFile, ex);
}
try {
fos.getChannel().transferFrom(channel, 0, Long.MAX_VALUE);
System.out.println("Downloaded to " + targetFile);
} catch (IOException ex) {
throw new UpdateException("Failed to get the specified file " + url + " to: " + targetFile, ex);
}
return targetFile.toPath();
}
}