diff --git a/foundation/plugins/org.jboss.tools.foundation.core/.options b/foundation/plugins/org.jboss.tools.foundation.core/.options new file mode 100644 index 0000000000..99d8b264bf --- /dev/null +++ b/foundation/plugins/org.jboss.tools.foundation.core/.options @@ -0,0 +1,21 @@ +# Debugging options for the org.jboss.tools.foundation.core plugin + +# Turn on general debugging +org.jboss.tools.foundation.core/debug=true + +# Tracing options +org.jboss.tools.foundation.core/config=false +org.jboss.tools.foundation.core/info=false +org.jboss.tools.foundation.core/warning=false +org.jboss.tools.foundation.core/severe=false +org.jboss.tools.foundation.core/finest=false +org.jboss.tools.foundation.core/finer=false + +# Tracking of resources +org.jboss.tools.foundation.core/resources=false + +# Loading of extension points +org.jboss.tools.foundation.core/extension_point=false + +# listeners +org.jboss.tools.foundation.core/listeners=false diff --git a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/Trace.java b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/Trace.java new file mode 100644 index 0000000000..4786c38682 --- /dev/null +++ b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/Trace.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2003, 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 API and implementation + *******************************************************************************/ +package org.jboss.tools.foundation.core; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.osgi.service.debug.DebugOptions; +import org.eclipse.osgi.service.debug.DebugOptionsListener; + +/** + * Helper class to route trace output. + */ +public class Trace implements DebugOptionsListener { + + private static final SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy HH:mm.ss.SSS"); //$NON-NLS-1$ + + private static Set logged = new HashSet(); + + // tracing enablement flags + public static boolean CONFIG = false; + public static boolean INFO = false; + public static boolean WARNING = false; + public static boolean SEVERE = false; + public static boolean FINER = false; + public static boolean FINEST = false; + public static boolean RESOURCES = false; + public static boolean EXTENSION_POINT = false; + public static boolean LISTENERS = false; + + // tracing levels. One most exist for each debug option + public final static String STRING_CONFIG = "/config"; //$NON-NLS-1$ + public final static String STRING_INFO = "/info"; //$NON-NLS-1$ + public final static String STRING_WARNING = "/warning"; //$NON-NLS-1$ + public final static String STRING_SEVERE = "/severe"; //$NON-NLS-1$ + public final static String STRING_FINER = "/finer"; //$NON-NLS-1$ + public final static String STRING_FINEST = "/finest"; //$NON-NLS-1$ + public final static String STRING_RESOURCES = "/resources"; //$NON-NLS-1$ + public final static String STRING_EXTENSION_POINT = "/extension_point"; //$NON-NLS-1$ + public final static String STRING_LISTENERS = "/listeners"; //$NON-NLS-1$ + + /** + * Trace constructor. This should never be explicitly called by clients and is used to register this class with the + * {@link DebugOptions} service. + */ + public Trace() { + super(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.osgi.service.debug.DebugOptionsListener#optionsChanged(org.eclipse.osgi.service.debug.DebugOptions) + */ + public void optionsChanged(DebugOptions options) { + Trace.CONFIG = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_CONFIG, false); + Trace.INFO = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_INFO, false); + Trace.WARNING = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_WARNING, false); + Trace.SEVERE = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_SEVERE, false); + Trace.FINER = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_FINER, false); + Trace.FINEST = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_FINEST, false); + Trace.RESOURCES = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_RESOURCES, false); + Trace.EXTENSION_POINT = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_EXTENSION_POINT, false); + Trace.LISTENERS = options.getBooleanOption(FoundationCorePlugin.PLUGIN_ID + Trace.STRING_LISTENERS, false); + } + + /** + * Trace the given message. + * + * @param level + * The tracing level. + * @param s + * The message to trace + */ + public static void trace(final String level, String s) { + + Trace.trace(level, s, null); + } + + /** + * Trace the given message and exception. + * + * @param level + * The tracing level. + * @param s + * The message to trace + * @param t + * A {@link Throwable} to trace + */ + public static void trace(final String level, String s, Throwable t) { + if (s == null) { + return; + } + if (Trace.STRING_SEVERE.equals(level)) { + if (!logged.contains(s)) { + FoundationCorePlugin.pluginLog().logError(s, t); + logged.add(s); + } + } + if (FoundationCorePlugin.getDefault().isDebugging()) { + final StringBuilder sb = new StringBuilder(FoundationCorePlugin.PLUGIN_ID); + sb.append(" "); //$NON-NLS-1$ + sb.append(level); + sb.append(" "); //$NON-NLS-1$ + sb.append(sdf.format(new Date())); + sb.append(" "); //$NON-NLS-1$ + sb.append(s); + System.out.println(sb.toString()); + if (t != null) { + t.printStackTrace(); + } + } + } + + +} \ No newline at end of file diff --git a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/InternalURLTransport.java b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/InternalURLTransport.java index 895e47f6bf..9160f461e4 100644 --- a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/InternalURLTransport.java +++ b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/InternalURLTransport.java @@ -61,6 +61,7 @@ import org.eclipse.equinox.security.storage.StorageException; import org.eclipse.osgi.util.NLS; import org.jboss.tools.foundation.core.FoundationCorePlugin; +import org.jboss.tools.foundation.core.Trace; import org.jboss.tools.foundation.core.ecf.Messages; import org.jboss.tools.foundation.core.jobs.BarrierWaitJob; import org.osgi.framework.Bundle; @@ -136,6 +137,7 @@ public static synchronized InternalURLTransport getInstance() { * @exception OperationCanceledException if the request was canceled. */ public long getLastModified(URL location, IProgressMonitor monitor) throws CoreException { + Trace.trace(Trace.STRING_FINER, "Checking last-modified timestamp for " + location.toExternalForm()); String locationString = location.toExternalForm(); try { IConnectContext context = getConnectionContext(locationString, false); @@ -190,6 +192,8 @@ private long doGetLastModified(String location, IConnectContext context, IProgre * to download or not. */ public IStatus download(String name, String url, OutputStream destination, int timeout, IProgressMonitor monitor) { + Trace.trace(Trace.STRING_FINER, "Downloading url " + url + " to an outputstream"); + IStatus status = null; try { IConnectContext context = getConnectionContext(url, false); @@ -274,6 +278,8 @@ public IStatus performDownload(String name,String toDownload, OutputStream targe private IStatus transfer(final String name,final IRetrieveFileTransferContainerAdapter retrievalContainer, final String toDownload, final OutputStream target, IConnectContext context, int timeout, final IProgressMonitor monitor) throws ProtocolException { + Trace.trace(Trace.STRING_FINER, "Beginning transfer for remote file " + toDownload); + final IStatus[] result = new IStatus[1]; final IIncomingFileTransferReceiveStartEvent[] cancelable = new IIncomingFileTransferReceiveStartEvent[1]; cancelable[0] = null; @@ -281,8 +287,17 @@ private IStatus transfer(final String name,final IRetrieveFileTransferContainerA IFileTransferListener listener = new IFileTransferListener() { private long transferStartTime; protected int oldWorked; + + // A temporary variable used to store work count + protected int tmpWorled; + + // Ensure no updates to status until 250 ms due to possible mac bug with too many updates + private long throttleMilliseconds = 250; + private long lastProgressUpdate = System.currentTimeMillis(); + public void handleTransferEvent(IFileTransferEvent event) { if (event instanceof IIncomingFileTransferReceiveStartEvent) { + Trace.trace(Trace.STRING_FINER, "Transfer has begun for " + toDownload); if( monitor.isCanceled()) { synchronized(result) { result[0] = FoundationCorePlugin.statusFactory().cancelStatus(Messages.ECFTransport_Operation_canceled); @@ -302,6 +317,7 @@ public void handleTransferEvent(IFileTransferEvent event) { final long totalWork = ((fileLength == -1) ? 100 : fileLength); int work = (totalWork > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) totalWork; monitor.beginTask(NLS.bind(Messages.ECFExamplesTransport_Downloading, name), work); + lastProgressUpdate = System.currentTimeMillis(); oldWorked=0; } } catch (IOException e) { @@ -328,30 +344,41 @@ public void handleTransferEvent(IFileTransferEvent event) { } return; } - long fileLength = source.getFileLength(); - final long totalWork = ((fileLength == -1) ? 100 : fileLength); - double factor = (totalWork > Integer.MAX_VALUE) ? (((double) Integer.MAX_VALUE) / ((double) totalWork)) : 1.0; - long received = source.getBytesReceived(); - int worked = (int) Math.round(factor * received); - double downloadRateBytesPerSecond = (received / ((System.currentTimeMillis() + 1 - transferStartTime) / 1000.0)); - String downloadRateString = AbstractRetrieveFileTransfer.toHumanReadableBytes(downloadRateBytesPerSecond); - String receivedString = AbstractRetrieveFileTransfer.toHumanReadableBytes(received); - String fileLengthString = AbstractRetrieveFileTransfer.toHumanReadableBytes(fileLength); - if( fileLength > 0 ) { - monitor.subTask(NLS.bind( - Messages.ECFExamplesTransport_ReceivedSize_At_RatePerSecond, - new String[]{receivedString, downloadRateString})); - } else { - monitor.subTask(NLS.bind( - Messages.ECFExamplesTransport_ReceivedSize_Of_FileSize_At_RatePerSecond, - new String[]{receivedString, fileLengthString, downloadRateString})); + long currentTime = System.currentTimeMillis(); + if( lastProgressUpdate + throttleMilliseconds < currentTime ) { + long fileLength = source.getFileLength(); + final long totalWork = ((fileLength == -1) ? 100 : fileLength); + double factor = (totalWork > Integer.MAX_VALUE) ? (((double) Integer.MAX_VALUE) / ((double) totalWork)) : 1.0; + long received = source.getBytesReceived(); + int worked = (int) Math.round(factor * received); + double downloadRateBytesPerSecond = (received / ((System.currentTimeMillis() + 1 - transferStartTime) / 1000.0)); + + String downloadRateString = AbstractRetrieveFileTransfer.toHumanReadableBytes(downloadRateBytesPerSecond); + String receivedString = AbstractRetrieveFileTransfer.toHumanReadableBytes(received); + String fileLengthString = AbstractRetrieveFileTransfer.toHumanReadableBytes(fileLength); + + String str = null; + if( fileLength < 0 ) { + str = NLS.bind( + Messages.ECFExamplesTransport_ReceivedSize_At_RatePerSecond, + new String[]{receivedString, downloadRateString}); + } else { + str = NLS.bind( + Messages.ECFExamplesTransport_ReceivedSize_Of_FileSize_At_RatePerSecond, + new String[]{receivedString, fileLengthString, downloadRateString}); + } + + Trace.trace(Trace.STRING_FINEST, "Transfer " + toDownload + " status: " + str); + monitor.subTask(str); + lastProgressUpdate = currentTime; + monitor.worked(worked-oldWorked); + oldWorked=worked; } - monitor.worked(worked-oldWorked); - oldWorked=worked; } } if (event instanceof IIncomingFileTransferReceiveDoneEvent) { + Trace.trace(Trace.STRING_FINER, "Transfer " + toDownload + " is complete"); Exception exception = ((IIncomingFileTransferReceiveDoneEvent) event).getException(); IStatus status = convertToStatus(exception); synchronized (result) { @@ -547,6 +574,7 @@ private IStatus convertToStatus(Exception e) { * Waits until the first entry in the given array is non-null. */ private void waitFor(String location, Object[] barrier) throws InterruptedException { + Trace.trace(Trace.STRING_FINER, "Waiting for remote file to download: " + location); BarrierWaitJob.waitForSynchronous(Messages.ECFExamplesTransport_Loading, barrier, true); } @@ -554,6 +582,7 @@ private synchronized ServiceTracker getFileTransferServiceTracker() { if (retrievalFactoryTracker == null) { retrievalFactoryTracker = new ServiceTracker(FoundationCorePlugin.getDefault().getBundleContext(), IRetrieveFileTransferFactory.class.getName(), null); retrievalFactoryTracker.open(); + Trace.trace(Trace.STRING_FINER, "Ensuring all bundles for URLTransport are started"); requestStart(BUNDLE_ECF); //$NON-NLS-1$ requestStart(BUNDLE_ECF_FILETRANSFER); //$NON-NLS-1$ } @@ -623,6 +652,7 @@ private boolean requestStart(String bundleId, Version version) { * @return boolean whether the bundle is now active */ private boolean forceStart(String bundleId) { + Trace.trace(Trace.STRING_FINEST, "Forcing " + bundleId + " to start"); Bundle bundle = Platform.getBundle(bundleId); //$NON-NLS-1$ if (bundle != null && bundle.getState() != Bundle.ACTIVE) { try { @@ -633,4 +663,4 @@ private boolean forceStart(String bundleId) { } return bundle.getState() == Bundle.ACTIVE; } -} \ No newline at end of file +} diff --git a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/URLTransportCache.java b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/URLTransportCache.java index 7fd5c0f4c9..a7ea9ab870 100644 --- a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/URLTransportCache.java +++ b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/ecf/internal/URLTransportCache.java @@ -29,6 +29,7 @@ import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.jboss.tools.foundation.core.FoundationCorePlugin; +import org.jboss.tools.foundation.core.Trace; import org.jboss.tools.foundation.core.ecf.Messages; import org.jboss.tools.foundation.core.ecf.URLTransportUtility; import org.osgi.service.prefs.BackingStoreException; @@ -72,6 +73,7 @@ public File getCachedFile(String url) { public boolean isCacheOutdated(String url, IProgressMonitor monitor) throws CoreException { + Trace.trace(Trace.STRING_FINER, "Checking if cache is outdated for " + url); File f = getCachedFile(url); if (f == null) return true; @@ -109,6 +111,8 @@ public boolean isCacheOutdated(String url, IProgressMonitor monitor) public File downloadAndCache(String url, String displayName, int lifespan, URLTransportUtility util, IProgressMonitor monitor) throws CoreException { + Trace.trace(Trace.STRING_FINER, "Downloading and caching " + url + " with lifespan=" + lifespan); + File target = getRemoteFileCacheLocation(url); try { OutputStream os = new FileOutputStream(target); @@ -134,18 +138,19 @@ private void addToCache(String url, File target) { private void loadPreferences() { IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(FoundationCorePlugin.PLUGIN_ID); String val = prefs.get(CACHE_MAP_KEY, ""); - if( isEmpty(val)) - return; - String[] byLine = val.split("\n"); - for( int i = 0; i < byLine.length; i++ ) { - if( isEmpty(byLine[i])) - continue; - String[] kv = byLine[i].split("="); - if( kv.length == 2 && !isEmpty(kv[0]) && !isEmpty(kv[1])) { - if( new File(kv[1]).exists() ) - cache.put(kv[0],kv[1]); + if( !isEmpty(val)) { + String[] byLine = val.split("\n"); + for( int i = 0; i < byLine.length; i++ ) { + if( isEmpty(byLine[i])) + continue; + String[] kv = byLine[i].split("="); + if( kv.length == 2 && !isEmpty(kv[0]) && !isEmpty(kv[1])) { + if( new File(kv[1]).exists() ) + cache.put(kv[0],kv[1]); + } } } + Trace.trace(Trace.STRING_FINER, "Loaded " + cache.size() + " cache file locations from preferences"); } private boolean isEmpty(String s) { @@ -157,6 +162,8 @@ private void savePreferences() { // saves plugin preferences at the workspace level IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(FoundationCorePlugin.PLUGIN_ID); + Trace.trace(Trace.STRING_FINER, "Saving " + cache.size() + " cache file locations to preferences"); + StringBuffer sb = new StringBuffer(); Iterator it = cache.keySet().iterator(); while(it.hasNext()) { diff --git a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierProgressWaitJob.java b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierProgressWaitJob.java index 7ce91d1259..624d35604d 100644 --- a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierProgressWaitJob.java +++ b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierProgressWaitJob.java @@ -14,6 +14,7 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; +import org.jboss.tools.foundation.core.Trace; /** @@ -79,6 +80,7 @@ public BarrierProgressWaitJob(String name, IRunnableWithProgress runnable) { } protected IStatus run(IProgressMonitor monitor) { + Trace.trace(Trace.STRING_FINER, "Launching job " + getName() + ", a BarrierProgressWaitJob"); try { Object ret = runnable.run(monitor); synchronized(barrier) { @@ -87,6 +89,7 @@ protected IStatus run(IProgressMonitor monitor) { return Status.OK_STATUS; } }catch(Exception e) { + Trace.trace(Trace.STRING_FINER, "Job " + getName() + ", a BarrierProgressWaitJob, failed with exception " + e.getMessage()); this.throwableCaught = true; synchronized(barrier) { barrier.notify(); @@ -120,13 +123,17 @@ public void monitorSafeJoin(IProgressMonitor monitor) { } catch (InterruptedException e) { } if( barrier[0] != null || monitor.isCanceled()) { + Trace.trace(Trace.STRING_FINER, "job " + getName() + ", a BarrierProgressWaitJob, is finished due to " + + (barrier[0] != null ? "a non-null result" : "a canceled progress monitor")); done = true; } } } // If this monitor is canceled, cancel the job immediately - if( monitor.isCanceled()) + if( monitor.isCanceled()) { + Trace.trace(Trace.STRING_FINER, "Progress monitor for job " + getName() + ", a BarrierProgressWaitJob, has been canceled"); cancel(); + } } /** diff --git a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierWaitJob.java b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierWaitJob.java index f8da14d0c1..573a202f76 100644 --- a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierWaitJob.java +++ b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/BarrierWaitJob.java @@ -13,6 +13,7 @@ import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; +import org.jboss.tools.foundation.core.Trace; /** * This class is a Job with the purpose of specifically @@ -47,12 +48,14 @@ public static void waitForSynchronous(String jobName, Object[] barrier, // Do NOT log the error. Let the caller log it as they wish. // Clean up the job, since I'm the only one who has a reference to it. wait.cancel(); - + Trace.trace(Trace.STRING_FINER, "Canceling the barrier wait job due to interrupted flag: " + wait.getName()); + // re-throw the interrupted exception, so whoever was waiting // knows to clean up if they can. throw e; } if( wait.hasBeenCanceled()) { + Trace.trace(Trace.STRING_FINER, "Job " + wait.getName() + " has been canceled"); throw new InterruptedException(); } } @@ -103,6 +106,7 @@ protected IStatus run(IProgressMonitor monitor) { */ protected void canceling() { synchronized( barrier ) { + Trace.trace(Trace.STRING_FINER, "Job " + getName() + " has been canceled. Notifying the barrier"); canceled = true; barrier.notify(); } diff --git a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/InterruptableJoinJob.java b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/InterruptableJoinJob.java index 5c4df55340..7d1d82e49e 100644 --- a/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/InterruptableJoinJob.java +++ b/foundation/plugins/org.jboss.tools.foundation.core/src/org/jboss/tools/foundation/core/jobs/InterruptableJoinJob.java @@ -17,6 +17,7 @@ import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.jboss.tools.foundation.core.Trace; /** * This is a job which provides a interruptableJoin() method to make it interruptable. @@ -57,29 +58,33 @@ public void interruptableJoin() throws InterruptedException { * A custom implementation of join because the official one * cannot be interrupted at all. [293312] * - * This implementation will be sure to add the listener BEFORE schedule, - * to prevent any issues for fast-completion jobs + * If schedule is true, this implementation will be sure to add the listener BEFORE schedule, + * to prevent any issues for fast-completion jobs. + * If schedule is false, the user must schedule the job themselves. * * @throws InterruptedException */ public void interruptableJoin(boolean schedule) throws InterruptedException { - + Trace.trace(Trace.STRING_FINER, "Joining job " + getName() + " in interruptable fashion"); final IJobChangeListener listener; final Semaphore barrier2; barrier2 = new Semaphore(null); listener = new JobChangeAdapter() { public void done(IJobChangeEvent event) { + Trace.trace(Trace.STRING_FINER, "Job " + event.getJob().getName() + " completed. Releasing barrier."); barrier2.release(); } }; addJobChangeListener(listener); - if( schedule ) + if( schedule ) { + Trace.trace(Trace.STRING_FINER, "Scheduling Job " + getName()); schedule(); - + } try { if (barrier2.acquire(Long.MAX_VALUE)) return; } catch (InterruptedException e) { + Trace.trace(Trace.STRING_FINER, "Job " + getName() + " has been interrupted, so this join is terminating"); // Actual join implementation LOOPS here, and ignores the exception. throw new InterruptedException(); }