/******************************************************************************* * Copyright (c) 2007, 2011 IBM Corporation and others. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial implementation and ideas ******************************************************************************/ package org.eclipse.equinox.internal.p2.reconciler.dropins; import java.io.*; import java.net.URI; import java.net.URL; import java.util.*; import org.eclipse.core.runtime.*; import org.eclipse.equinox.internal.p2.artifact.repository.ArtifactRepositoryManager; import org.eclipse.equinox.internal.p2.core.helpers.*; import org.eclipse.equinox.internal.p2.extensionlocation.*; import org.eclipse.equinox.internal.p2.metadata.repository.MetadataRepositoryManager; import org.eclipse.equinox.internal.p2.reconciler.dropins.DropinsRepositoryListener.LinkedRepository; import org.eclipse.equinox.internal.p2.update.Configuration; import org.eclipse.equinox.internal.p2.update.PathUtil; import org.eclipse.equinox.internal.provisional.p2.directorywatcher.DirectoryWatcher; import org.eclipse.equinox.p2.core.IProvisioningAgent; import org.eclipse.equinox.p2.core.ProvisionException; import org.eclipse.equinox.p2.engine.IProfile; import org.eclipse.equinox.p2.engine.IProfileRegistry; import org.eclipse.equinox.p2.repository.IRepository; import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository; import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager; import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository; import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager; import org.eclipse.osgi.service.datalocation.Location; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { static final String PROP_APPLICATION_STATUS = "org.eclipse.equinox.p2.reconciler.application.status"; //$NON-NLS-1$ public static final String ID = "org.eclipse.equinox.p2.reconciler.dropins"; //$NON-NLS-1$ private static final String DROPINS_DIRECTORY = "org.eclipse.equinox.p2.reconciler.dropins.directory"; //$NON-NLS-1$ private static final String DROPINS = "dropins"; //$NON-NLS-1$ private static final String LINKS = "links"; //$NON-NLS-1$ private static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$ private static final String PLATFORM_CFG = "org.eclipse.update/platform.xml"; //$NON-NLS-1$ private static final String CACHE_FILENAME = "cache.timestamps"; //$NON-NLS-1$ private static final String DIR_ECLIPSE = "eclipse"; //$NON-NLS-1$ private static final String DIR_PLUGINS = "plugins"; //$NON-NLS-1$ private static final String DIR_FEATURES = "features"; //$NON-NLS-1$ private static final String EXT_LINK = ".link"; //$NON-NLS-1$ public static final String TRACING_PREFIX = "[reconciler] "; //$NON-NLS-1$ private static BundleContext bundleContext; private final static Set<IMetadataRepository> repositories = new HashSet<IMetadataRepository>(); private Collection<File> filesToCheck = null; /** * Helper method to create an extension location metadata repository at the given URI. * If one already exists at that location then an exception will be thrown. * * This method never returns <code>null</code>. * * @throws IllegalStateException * @throws ProvisionException */ public static IMetadataRepository createExtensionLocationMetadataRepository(URI location, String name, Map<String, String> properties) throws ProvisionException { IProvisioningAgent agent = getAgent(); IMetadataRepositoryManager manager = (IMetadataRepositoryManager) agent.getService(IMetadataRepositoryManager.SERVICE_NAME); if (manager == null) throw new IllegalStateException("MetadataRepositoryManager not registered."); //$NON-NLS-1$ ExtensionLocationMetadataRepositoryFactory factory = new ExtensionLocationMetadataRepositoryFactory(); factory.setAgent(agent); // always compress repositories that we are creating. Map<String, String> repositoryProperties = new HashMap<String, String>(); repositoryProperties.put(IRepository.PROP_COMPRESSED, Boolean.TRUE.toString()); if (properties != null) repositoryProperties.putAll(properties); IMetadataRepository repository = factory.create(location, name, ExtensionLocationMetadataRepository.TYPE, repositoryProperties); //we need to add the concrete repository to the repository manager, or its properties will not be correct ((MetadataRepositoryManager) manager).addRepository(repository); manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true)); return repository; } private static IProvisioningAgent getAgent() { return (IProvisioningAgent) ServiceHelper.getService(getContext(), IProvisioningAgent.SERVICE_NAME); } /** * Helper method to load an extension location metadata repository from the given URL. * * @throws IllegalStateException * @throws ProvisionException */ public static IMetadataRepository loadMetadataRepository(URI location, IProgressMonitor monitor) throws ProvisionException { IMetadataRepositoryManager manager = (IMetadataRepositoryManager) getAgent().getService(IMetadataRepositoryManager.SERVICE_NAME); if (manager == null) throw new IllegalStateException("MetadataRepositoryManager not registered."); //$NON-NLS-1$ IMetadataRepository repository = manager.loadRepository(location, monitor); manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true)); return repository; } /** * Helper method to create an extension location artifact repository at the given URL. * If one already exists at that location then an exception will be thrown. * * This method never returns <code>null</code>. * * @throws IllegalStateException * @throws ProvisionException */ public static IArtifactRepository createExtensionLocationArtifactRepository(URI location, String name, Map<String, String> properties) throws ProvisionException { IProvisioningAgent agent = getAgent(); IArtifactRepositoryManager manager = (IArtifactRepositoryManager) agent.getService(IArtifactRepositoryManager.SERVICE_NAME); if (manager == null) throw new IllegalStateException("ArtifactRepositoryManager not registered."); //$NON-NLS-1$ ExtensionLocationArtifactRepositoryFactory factory = new ExtensionLocationArtifactRepositoryFactory(); factory.setAgent(agent); // always compress repositories that we are creating. Map<String, String> repositoryProperties = new HashMap<String, String>(); repositoryProperties.put(IRepository.PROP_COMPRESSED, Boolean.TRUE.toString()); if (properties != null) repositoryProperties.putAll(properties); IArtifactRepository repository = factory.create(location, name, ExtensionLocationArtifactRepository.TYPE, repositoryProperties); //we need to add the concrete repository to the repository manager, or its properties will not be correct ((ArtifactRepositoryManager) manager).addRepository(repository); manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true)); return repository; } /** * Helper method to load an extension location metadata repository from the given URL. * * @throws IllegalStateException * @throws ProvisionException */ public static IArtifactRepository loadArtifactRepository(URI location, IProgressMonitor monitor) throws ProvisionException { IArtifactRepositoryManager manager = (IArtifactRepositoryManager) getAgent().getService(IArtifactRepositoryManager.SERVICE_NAME); if (manager == null) throw new IllegalStateException("ArtifactRepositoryManager not registered."); //$NON-NLS-1$ IArtifactRepository repository = manager.loadRepository(location, monitor); manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true)); return repository; } /* * Return the set of metadata repositories known to this bundle. It is constructed from the repos * for the drop-ins as well as the ones in the configuration. */ public static Set<IMetadataRepository> getRepositories() { return repositories; } /* (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { bundleContext = context; // check to see if there is really any work to do. Do this after setting the context, and // doing other initialization in case others call our public methods later. if (isUpToDate()) { // clear the cache filesToCheck = null; return; } checkConfigIni(); // create the watcher for the "drop-ins" folder watchDropins(); // keep an eye on the platform.xml watchConfiguration(); synchronize(null); writeTimestamps(); // we should probably be holding on to these repos by URL // see Bug 223422 // for now explicitly nulling out these repos to allow GC to occur repositories.clear(); filesToCheck = null; } private void checkConfigIni() { File configuration = getConfigurationLocation(); if (configuration == null) { LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to determine configuration location.")); //$NON-NLS-1$ return; } File configIni = new File(configuration, CONFIG_INI); if (!configIni.exists()) { // try parent configuration File parentConfiguration = getParentConfigurationLocation(); if (parentConfiguration == null) return; // write shared configuration Properties props = new Properties(); try { OutputStream os = null; try { os = new BufferedOutputStream(new FileOutputStream(configIni)); String externalForm = PathUtil.makeRelative(parentConfiguration.toURL().toExternalForm(), getOSGiInstallArea()).replace('\\', '/'); props.put("osgi.sharedConfiguration.area", externalForm); //$NON-NLS-1$ props.store(os, "Linked configuration"); //$NON-NLS-1$ } finally { if (os != null) os.close(); } } catch (IOException e) { LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to create linked configuration location.", e)); //$NON-NLS-1$ } } } /* * Return a boolean value indicating whether or not we need to run * the reconciler due to changes in the file-system. */ private boolean isUpToDate() { // the user might want to force a reconciliation if ("true".equals(getContext().getProperty("osgi.checkConfiguration"))) { //$NON-NLS-1$//$NON-NLS-2$ trace("User requested forced reconciliation via \"osgi.checkConfiguration=true\" System property."); //$NON-NLS-1$ trace("Performing reconciliation."); //$NON-NLS-1$ return false; } // read timestamps Properties timestamps = readTimestamps(); if (timestamps.isEmpty()) { trace("Cached timestamp file empty."); //$NON-NLS-1$ trace("Performing reconciliation."); //$NON-NLS-1$ return false; } // gather the list of files/folders that we need to check Collection<File> files = getFilesToCheck(); for (Iterator<File> iter = files.iterator(); iter.hasNext();) { File file = iter.next(); String key = file.getAbsolutePath(); String timestamp = timestamps.getProperty(key); if (timestamp == null) { trace("Missing timestamp for file: " + key); //$NON-NLS-1$ trace("Performing reconciliation."); //$NON-NLS-1$ return false; } long lastModified = file.lastModified(); if (!Long.toString(lastModified).equals(timestamp)) { trace("Timestamp has been updated for file: " + key + ", expected: " + timestamp + ", actual: " + lastModified); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ trace("Performing reconciliation."); //$NON-NLS-1$ return false; } timestamps.remove(key); } // if we had some extra timestamps in the file, then signal that something has // changed and we need to reconcile boolean result = timestamps.isEmpty(); if (result) { trace("Cached timestamp values up to date."); //$NON-NLS-1$ trace("Reconciliation skipped."); //$NON-NLS-1$ } else { if (Tracing.DEBUG_RECONCILER) { trace("Found extra values in cached timestamp file: "); //$NON-NLS-1$ for (Iterator<Object> iter = timestamps.keySet().iterator(); iter.hasNext();) trace(iter.next()); trace("Performing reconciliation. "); //$NON-NLS-1$ } } return result; } /* * Restore the cached timestamp values. */ private Properties readTimestamps() { Properties result = new Properties(); File file = Activator.getContext().getDataFile(CACHE_FILENAME); if (!file.exists()) return result; trace("Reading timestamps from file: " + file.getAbsolutePath()); //$NON-NLS-1$ InputStream input = null; try { input = new BufferedInputStream(new FileInputStream(file)); result.load(input); } catch (IOException e) { LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Error occurred while reading cached timestamps for reconciliation.", e)); //$NON-NLS-1$ } finally { try { if (input != null) input.close(); } catch (IOException e) { // ignore } } if (Tracing.DEBUG_RECONCILER) { for (Iterator<Object> iter = result.keySet().iterator(); iter.hasNext();) { Object key = iter.next(); Object value = result.get(key); trace(key.toString() + '=' + value); } } return result; } /* * Return a collection of files which are interesting to us when we want to record timestamps * to figure out if something has changed and perhaps avoid an unnecessary reconcilation. */ private Collection<File> getFilesToCheck() { if (filesToCheck != null) return filesToCheck; Set<File> result = new HashSet<File>(); // configuration/org.eclipse.update/platform.xml, configuration/../plugins, configuration/../features File configuration = getConfigurationLocation(); if (configuration != null) { result.add(new File(configuration, PLATFORM_CFG)); File parent = configuration.getParentFile(); if (parent != null) { File plugins = new File(parent, "plugins"); //$NON-NLS-1$ result.add(plugins); File features = new File(parent, "features"); //$NON-NLS-1$ result.add(features); } } // if we are in shared mode then record the same files for the parent configuration File parentConfiguration = getParentConfigurationLocation(); if (parentConfiguration != null) { result.add(new File(parentConfiguration, PLATFORM_CFG)); File parent = parentConfiguration.getParentFile(); if (parent != null) { File plugins = new File(parent, "plugins"); //$NON-NLS-1$ result.add(plugins); File features = new File(parent, "features"); //$NON-NLS-1$ result.add(features); } } // dropins folders File[] dropins = getDropinsDirectories(); result.addAll(getDropinsToCheck(dropins)); // links folders File[] links = getLinksDirectories(); result.addAll(getDropinsToCheck(links)); filesToCheck = result; return filesToCheck; } /* * Iterate over the given collection of files (could be dropins or links folders) and * return a collection of files that might be interesting to check the timestamps of. */ private Collection<File> getDropinsToCheck(File[] files) { Collection<File> result = new HashSet<File>(); for (int outer = 0; outer < files.length; outer++) { // add top-level file/folder result.add(files[outer]); File[] children = files[outer].listFiles(); for (int inner = 0; children != null && inner < children.length; inner++) { File child = children[inner]; if (child.isFile() && child.getName().toLowerCase().endsWith(EXT_LINK)) { // if we have a link file then add the link file and its target LinkedRepository repo = DropinsRepositoryListener.getLinkedRepository(child); if (repo == null || !repo.exists()) continue; File target = repo.getLocation(); result.add(child); result.add(target); File eclipse = new File(target, DIR_ECLIPSE); result.add(eclipse); result.add(new File(eclipse, DIR_PLUGINS)); result.add(new File(eclipse, DIR_FEATURES)); } else if (child.getName().equalsIgnoreCase(DIR_ECLIPSE)) { // if it is an "eclipse" dir then add it as well as "plugins" and "features" result.add(child); result.add(new File(child, DIR_PLUGINS)); result.add(new File(child, DIR_FEATURES)); } else if (child.isDirectory()) { // look for "dropins/foo/plugins" (and "features") and // "dropins/foo/eclipse/plugins" (and "features") // Note: we could have a directory-based bundle here but we // will still add it since it won't hurt anything (one extra timestamp check) result.add(child); File parent; File eclipse = new File(child, DIR_ECLIPSE); if (eclipse.exists()) { result.add(eclipse); parent = eclipse; } else { parent = child; } File plugins = new File(parent, DIR_PLUGINS); if (plugins.exists()) result.add(plugins); File features = new File(parent, DIR_FEATURES); if (features.exists()) result.add(features); } } } return result; } /* * Persist the cache timestamp values. */ private void writeTimestamps() { Properties timestamps = new Properties(); Collection<File> files = getFilesToCheck(); for (Iterator<File> iter = files.iterator(); iter.hasNext();) { File file = iter.next(); timestamps.put(file.getAbsolutePath(), Long.toString(file.lastModified())); } // write out the file File file = Activator.getContext().getDataFile(CACHE_FILENAME); trace("Writing out timestamps to file : " + file.getAbsolutePath()); //$NON-NLS-1$ OutputStream output = null; try { file.delete(); output = new BufferedOutputStream(new FileOutputStream(file)); timestamps.store(output, null); if (Tracing.DEBUG_RECONCILER) { for (Iterator<Object> iter = timestamps.keySet().iterator(); iter.hasNext();) { Object key = iter.next(); Object value = timestamps.get(key); trace(key.toString() + '=' + value); } } } catch (IOException e) { LogHelper.log(new Status(IStatus.ERROR, ID, "Error occurred while writing cache timestamps for reconciliation.", e)); //$NON-NLS-1$ } finally { if (output != null) try { output.close(); } catch (IOException e) { // ignore } } } /* * Synchronize the profile. */ public static synchronized void synchronize(IProgressMonitor monitor) { IProfile profile = getCurrentProfile(bundleContext); if (profile == null) return; // create the profile synchronizer on all available repositories ProfileSynchronizer synchronizer = new ProfileSynchronizer(getAgent(), profile, repositories); IStatus result = synchronizer.synchronize(monitor); if (ProfileSynchronizer.isReconciliationApplicationRunning()) { System.getProperties().put(PROP_APPLICATION_STATUS, result); } if (!result.isOK() && !(result.getSeverity() == IStatus.CANCEL)) LogHelper.log(result); } /* * Watch the platform.xml file. */ private void watchConfiguration() { File configFile = getConfigurationLocation(); if (configFile == null) { LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to determine configuration location.")); //$NON-NLS-1$ return; } configFile = new File(configFile, PLATFORM_CFG); if (!configFile.exists()) { // try parent configuration File parentConfiguration = getParentConfigurationLocation(); if (parentConfiguration == null) return; File shareConfigFile = new File(parentConfiguration, PLATFORM_CFG); if (!shareConfigFile.exists()) return; Configuration config = new Configuration(); config.setDate(Long.toString(new Date().getTime())); config.setVersion("3.0"); //$NON-NLS-1$ try { String sharedUR = PathUtil.makeRelative(shareConfigFile.toURL().toExternalForm(), getOSGiInstallArea()).replace('\\', '/'); config.setSharedUR(sharedUR); // ensure that org.eclipse.update directory that holds platform.xml is pre-created. configFile.getParentFile().mkdirs(); config.save(configFile, getOSGiInstallArea()); } catch (IOException e) { LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to create linked platform.xml.", e)); //$NON-NLS-1$ return; } catch (ProvisionException e) { LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to create linked platform.xml.", e)); //$NON-NLS-1$ return; } } DirectoryWatcher watcher = new DirectoryWatcher(configFile.getParentFile()); PlatformXmlListener listener = new PlatformXmlListener(configFile); watcher.addListener(listener); watcher.poll(); repositories.addAll(listener.getMetadataRepositories()); } /* * Create a new directory watcher with a repository listener on the drop-ins folder. */ private void watchDropins() { List<File> directories = new ArrayList<File>(); File[] dropinsDirectories = getDropinsDirectories(); directories.addAll(Arrays.asList(dropinsDirectories)); File[] linksDirectories = getLinksDirectories(); directories.addAll(Arrays.asList(linksDirectories)); if (directories.isEmpty()) return; // we will compress the repositories and mark them hidden as "system" repos. Map<String, String> properties = new HashMap<String, String>(); properties.put(IRepository.PROP_COMPRESSED, Boolean.TRUE.toString()); properties.put(IRepository.PROP_SYSTEM, Boolean.TRUE.toString()); DropinsRepositoryListener listener = new DropinsRepositoryListener(getAgent(), DROPINS, properties); DirectoryWatcher watcher = new DirectoryWatcher(directories.toArray(new File[directories.size()])); watcher.addListener(listener); watcher.poll(); repositories.addAll(listener.getMetadataRepositories()); } /* (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { bundleContext = null; } /* * Return the bundle context for this bundle. */ public static BundleContext getContext() { return bundleContext; } /* * Helper method to get the configuration location. Return null if * it is unavailable. */ public static File getConfigurationLocation() { Location configurationLocation = (Location) ServiceHelper.getService(getContext(), Location.class.getName(), Location.CONFIGURATION_FILTER); if (configurationLocation == null || !configurationLocation.isSet()) return null; URL url = configurationLocation.getURL(); if (url == null) return null; return URLUtil.toFile(url); } /* * Helper method to get the shared configuration location. Return null if * it is unavailable. */ public static File getParentConfigurationLocation() { Location configurationLocation = (Location) ServiceHelper.getService(getContext(), Location.class.getName(), Location.CONFIGURATION_FILTER); if (configurationLocation == null || !configurationLocation.isSet()) return null; Location sharedConfigurationLocation = configurationLocation.getParentLocation(); if (sharedConfigurationLocation == null) return null; URL url = sharedConfigurationLocation.getURL(); if (url == null) return null; return URLUtil.toFile(url); } /* * Do a look-up and return the OSGi install area if it is set. */ public static URL getOSGiInstallArea() { Location location = (Location) ServiceHelper.getService(Activator.getContext(), Location.class.getName(), Location.INSTALL_FILTER); if (location == null) return null; if (!location.isSet()) return null; return location.getURL(); } /* * Perform variable substitution on the given string. Replace vars in the form %foo% * with the equivalent property set in the System properties. */ public static String substituteVariables(String path) { if (path == null) return path; int beginIndex = path.indexOf('%'); // no variable if (beginIndex == -1) return path; beginIndex++; int endIndex = path.indexOf('%', beginIndex); // no matching end % to indicate variable if (endIndex == -1) return path; // get the variable name and do a lookup String var = path.substring(beginIndex, endIndex); if (var.length() == 0 || var.indexOf(File.pathSeparatorChar) != -1) return path; var = getContext().getProperty(var); if (var == null) return path; return path.substring(0, beginIndex - 1) + var + path.substring(endIndex + 1); } /* * Helper method to return the eclipse.home location. Return * null if it is unavailable. */ public static File getEclipseHome() { Location eclipseHome = (Location) ServiceHelper.getService(getContext(), Location.class.getName(), Location.ECLIPSE_HOME_FILTER); if (eclipseHome == null || !eclipseHome.isSet()) return null; URL url = eclipseHome.getURL(); if (url == null) return null; return URLUtil.toFile(url); } /* * Return the locations of the links directories. There is a potential for * more than one to be returned here if we are running in shared mode. */ private static File[] getLinksDirectories() { List<File> linksDirectories = new ArrayList<File>(); File root = getEclipseHome(); if (root != null) linksDirectories.add(new File(root, LINKS)); // check to see if we are in shared mode. if so, then add the user's local // links directory. (the shared one will have been added above with the // reference to Eclipse home) if (getParentConfigurationLocation() != null) { File configuration = getConfigurationLocation(); if (configuration != null && configuration.getParentFile() != null) linksDirectories.add(new File(configuration.getParentFile(), LINKS)); } return linksDirectories.toArray(new File[linksDirectories.size()]); } /* * Return the location of the dropins directories. These include the one specified by * the "org.eclipse.equinox.p2.reconciler.dropins.directory" System property and the one * in the Eclipse home directory. If we are in shared mode, then also add the user's * local dropins directory. */ private static File[] getDropinsDirectories() { List<File> dropinsDirectories = new ArrayList<File>(); // did the user specify one via System properties? String watchedDirectoryProperty = bundleContext.getProperty(DROPINS_DIRECTORY); if (watchedDirectoryProperty != null) { // perform a variable substitution if necessary watchedDirectoryProperty = substituteVariables(watchedDirectoryProperty); dropinsDirectories.add(new File(watchedDirectoryProperty)); } // always add the one in the Eclipse home directory File root = getEclipseHome(); if (root != null) dropinsDirectories.add(new File(root, DROPINS)); // check to see if we are in shared mode. if so, then add the user's local // dropins directory. (the shared one will have been added above with the // reference to Eclipse home) if (getParentConfigurationLocation() != null) { File configuration = getConfigurationLocation(); if (configuration != null && configuration.getParentFile() != null) dropinsDirectories.add(new File(configuration.getParentFile(), DROPINS)); } return dropinsDirectories.toArray(new File[dropinsDirectories.size()]); } /* * Return the current profile or null if it cannot be retrieved. */ public static IProfile getCurrentProfile(BundleContext context) { IProvisioningAgent agent = getAgent(); IProfileRegistry profileRegistry = (IProfileRegistry) agent.getService(IProfileRegistry.SERVICE_NAME); if (profileRegistry == null) return null; return profileRegistry.getProfile(IProfileRegistry.SELF); } /* * If tracing is enabled, then write out the given message. */ public static void trace(Object message) { if (Tracing.DEBUG_RECONCILER) Tracing.debug(TRACING_PREFIX + message); } }