| /* |
| * Copyright (c) 2007-2014 Eike Stepper (Berlin, Germany) 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: |
| * Eike Stepper - initial API and implementation |
| */ |
| package org.eclipse.net4j.util.tests; |
| |
| import org.eclipse.net4j.internal.util.test.TestExecuter; |
| import org.eclipse.net4j.tests.bundle.OM; |
| import org.eclipse.net4j.util.ReflectUtil; |
| import org.eclipse.net4j.util.concurrent.ConcurrencyUtil; |
| import org.eclipse.net4j.util.concurrent.TrackableTimerTask; |
| import org.eclipse.net4j.util.event.EventUtil; |
| import org.eclipse.net4j.util.event.IEvent; |
| import org.eclipse.net4j.util.event.IListener; |
| import org.eclipse.net4j.util.event.INotifier; |
| import org.eclipse.net4j.util.io.IORuntimeException; |
| import org.eclipse.net4j.util.io.IOUtil; |
| import org.eclipse.net4j.util.io.TMPUtil; |
| import org.eclipse.net4j.util.lifecycle.ILifecycle; |
| import org.eclipse.net4j.util.lifecycle.LifecycleEventAdapter; |
| import org.eclipse.net4j.util.lifecycle.LifecycleUtil; |
| import org.eclipse.net4j.util.om.OMPlatform; |
| import org.eclipse.net4j.util.om.log.FileLogHandler; |
| import org.eclipse.net4j.util.om.log.OMLogger; |
| import org.eclipse.net4j.util.om.log.OMLogger.Level; |
| import org.eclipse.net4j.util.om.log.PrintLogHandler; |
| import org.eclipse.net4j.util.om.trace.ContextTracer; |
| import org.eclipse.net4j.util.om.trace.PrintTraceHandler; |
| |
| import org.junit.Assert; |
| |
| import java.io.File; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.locks.Condition; |
| import java.util.concurrent.locks.Lock; |
| |
| import junit.framework.AssertionFailedError; |
| import junit.framework.TestCase; |
| import junit.framework.TestResult; |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public abstract class AbstractOMTest extends TestCase |
| { |
| public static final long DEFAULT_TIMEOUT = 15 * 1000; |
| |
| public static final long DEFAULT_TIMEOUT_EXPECTED = 3 * 1000; |
| |
| public static boolean EXTERNAL_LOG; |
| |
| public static boolean SUPPRESS_OUTPUT; |
| |
| private static final IListener DUMPER = new IListener() |
| { |
| public void notifyEvent(IEvent event) |
| { |
| IOUtil.OUT().println(event); |
| } |
| }; |
| |
| private static final ContextTracer TRACER = new ContextTracer(OM.DEBUG, AbstractOMTest.class); |
| |
| private static boolean consoleEnabled; |
| |
| private static String testName; |
| |
| private transient List<File> filesToDelete = new ArrayList<File>(); |
| |
| private transient String codeLink; |
| |
| static |
| { |
| try |
| { |
| if (EXTERNAL_LOG) |
| { |
| SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); |
| String prefix = AbstractOMTest.class.getName() + "-" + formatter.format(new Date()) + "-"; |
| File logFile = TMPUtil.createTempFile(prefix, ".log"); |
| |
| OMPlatform.INSTANCE.addLogHandler(new FileLogHandler(logFile, OMLogger.Level.WARN) |
| { |
| @Override |
| protected void writeLog(OMLogger logger, Level level, String msg, Throwable t) throws Throwable |
| { |
| super.writeLog(logger, level, "--> " + testName + "\n" + msg, t); |
| } |
| }); |
| |
| IOUtil.ERR().println("Logging errors and warnings to " + logFile); |
| IOUtil.ERR().println(); |
| } |
| } |
| catch (Throwable ex) |
| { |
| IOUtil.print(ex); |
| } |
| } |
| |
| protected AbstractOMTest() |
| { |
| } |
| |
| public String getCodeLink() |
| { |
| return codeLink; |
| } |
| |
| public void determineCodeLink() |
| { |
| if (codeLink == null) |
| { |
| codeLink = determineCodeLink(getName()); |
| if (codeLink == null) |
| { |
| codeLink = determineCodeLink("doSetUp"); |
| if (codeLink == null) |
| { |
| codeLink = getClass().getName() + "." + getName() + "(" + getClass().getSimpleName() + ".java:1)"; |
| } |
| } |
| } |
| } |
| |
| protected String determineCodeLink(String methodName) |
| { |
| String className = getClass().getName(); |
| StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); |
| for (StackTraceElement frame : stackTrace) |
| { |
| if (frame.getClassName().equals(className) && frame.getMethodName().equals(methodName)) |
| { |
| return frame.toString(); |
| } |
| } |
| |
| return null; |
| } |
| |
| protected boolean logSetUpAndTearDown() |
| { |
| return false; |
| } |
| |
| @Override |
| public void setUp() throws Exception |
| { |
| testName = getClass().getName() + "." + getName() + "()"; |
| codeLink = null; |
| |
| PrintTraceHandler.CONSOLE.setShortContext(true); |
| OMPlatform.INSTANCE.addTraceHandler(PrintTraceHandler.CONSOLE); |
| OMPlatform.INSTANCE.addLogHandler(PrintLogHandler.CONSOLE); |
| enableConsole(); |
| |
| if (!SUPPRESS_OUTPUT) |
| { |
| IOUtil.OUT().println("*******************************************************"); //$NON-NLS-1$ |
| Thread.yield(); |
| Thread.sleep(2L); |
| IOUtil.ERR().println(this); |
| Thread.yield(); |
| Thread.sleep(2L); |
| IOUtil.OUT().println("*******************************************************"); //$NON-NLS-1$ |
| } |
| |
| if (!logSetUpAndTearDown()) |
| { |
| disableConsole(); |
| } |
| |
| super.setUp(); |
| doSetUp(); |
| |
| if (!SUPPRESS_OUTPUT && logSetUpAndTearDown()) |
| { |
| IOUtil.OUT().println(); |
| IOUtil.OUT().println("------------------------ START ------------------------"); //$NON-NLS-1$ |
| } |
| |
| enableConsole(); |
| } |
| |
| @Override |
| public void tearDown() throws Exception |
| { |
| if (logSetUpAndTearDown()) |
| { |
| enableConsole(); |
| } |
| else |
| { |
| disableConsole(); |
| } |
| |
| if (!SUPPRESS_OUTPUT && logSetUpAndTearDown()) |
| { |
| IOUtil.OUT().println("------------------------- END -------------------------"); //$NON-NLS-1$ |
| IOUtil.OUT().println(); |
| } |
| |
| try |
| { |
| doTearDown(); |
| } |
| catch (Exception ex) |
| { |
| IOUtil.print(ex); |
| } |
| |
| try |
| { |
| super.tearDown(); |
| } |
| catch (Exception ex) |
| { |
| IOUtil.print(ex); |
| } |
| |
| try |
| { |
| TrackableTimerTask.logConstructionStackTraces(2 * DEFAULT_TIMEOUT); |
| } |
| catch (Exception ex) |
| { |
| IOUtil.print(ex); |
| } |
| |
| try |
| { |
| clearReferences(getClass()); |
| } |
| catch (Exception ex) |
| { |
| IOUtil.print(ex); |
| } |
| |
| if (!SUPPRESS_OUTPUT) |
| { |
| IOUtil.OUT().println(); |
| IOUtil.OUT().println(); |
| } |
| } |
| |
| protected void clearReferences(Class<?> c) |
| { |
| if (c != AbstractOMTest.class) |
| { |
| for (Field field : c.getDeclaredFields()) |
| { |
| if (Modifier.isStatic(field.getModifiers())) |
| { |
| continue; |
| } |
| |
| if (field.getType().isPrimitive()) |
| { |
| continue; |
| } |
| |
| ReflectUtil.setValue(field, this, null); |
| } |
| |
| clearReferences(c.getSuperclass()); |
| } |
| } |
| |
| @Override |
| public void runBare() throws Throwable |
| { |
| TestExecuter.execute(this, new TestExecuter.Executable() |
| { |
| public void execute() throws Throwable |
| { |
| try |
| { |
| Throwable exception = null; |
| |
| try |
| { |
| setUp(); |
| |
| // Don't call super.runBare() because it does not clean up after exceptions from setUp() |
| runTest(); |
| } |
| catch (Throwable running) |
| { |
| exception = running; |
| } |
| finally |
| { |
| try |
| { |
| tearDown(); |
| } |
| catch (Throwable tearingDown) |
| { |
| if (exception == null) |
| { |
| exception = tearingDown; |
| } |
| } |
| } |
| |
| if (exception != null) |
| { |
| throw exception; |
| } |
| } |
| catch (SkipTestException ex) |
| { |
| OM.LOG.info("Skipped " + this); //$NON-NLS-1$ |
| } |
| catch (Throwable t) |
| { |
| // AssertionFailedError assertionFailedError = getAssertionFailedError(t); |
| // if (assertionFailedError != null) |
| // { |
| // t = assertionFailedError; |
| // } |
| |
| if (!SUPPRESS_OUTPUT) |
| { |
| t.printStackTrace(IOUtil.OUT()); |
| } |
| |
| throw t; |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void run(TestResult result) |
| { |
| try |
| { |
| super.run(result); |
| } |
| catch (SkipTestException ex) |
| { |
| OM.LOG.info("Skipped " + this); //$NON-NLS-1$ |
| } |
| catch (RuntimeException ex) |
| { |
| if (!SUPPRESS_OUTPUT) |
| { |
| ex.printStackTrace(IOUtil.OUT()); |
| } |
| |
| throw ex; |
| } |
| catch (Error err) |
| { |
| // AssertionFailedError assertionFailedError = getAssertionFailedError(err); |
| // if (assertionFailedError != null) |
| // { |
| // err = assertionFailedError; |
| // } |
| |
| if (!SUPPRESS_OUTPUT) |
| { |
| err.printStackTrace(IOUtil.OUT()); |
| } |
| |
| throw err; |
| } |
| } |
| |
| // private AssertionFailedError getAssertionFailedError(Throwable err) |
| // { |
| // if (err.getClass() == AssertionError.class) |
| // { |
| // // JUnit4 seems to throw java.lang.AssertionError, which the JUNit view displays as error rather than failure |
| // AssertionFailedError replacementError = new AssertionFailedError(err.getMessage()); |
| // replacementError.initCause(err); |
| // return replacementError; |
| // } |
| // |
| // if (err instanceof AssertionFailedError) |
| // { |
| // return (AssertionFailedError)err; |
| // } |
| // |
| // return null; |
| // } |
| |
| protected void enableConsole() |
| { |
| if (!SUPPRESS_OUTPUT) |
| { |
| OMPlatform.INSTANCE.setDebugging(true); |
| consoleEnabled = true; |
| } |
| } |
| |
| protected void disableConsole() |
| { |
| if (!SUPPRESS_OUTPUT) |
| { |
| consoleEnabled = false; |
| OMPlatform.INSTANCE.setDebugging(false); |
| // OMPlatform.INSTANCE.removeTraceHandler(PrintTraceHandler.CONSOLE); |
| // OMPlatform.INSTANCE.removeLogHandler(PrintLogHandler.CONSOLE); |
| } |
| } |
| |
| protected void doSetUp() throws Exception |
| { |
| } |
| |
| protected void doTearDown() throws Exception |
| { |
| deleteFiles(); |
| } |
| |
| public void deleteFiles() |
| { |
| for (File file : filesToDelete) |
| { |
| IOUtil.delete(file); |
| } |
| |
| filesToDelete.clear(); |
| } |
| |
| public void addFileToDelete(File file) |
| { |
| filesToDelete.add(file); |
| } |
| |
| public File createTempFolder() throws IORuntimeException |
| { |
| File folder = TMPUtil.createTempFolder(); |
| addFileToDelete(folder); |
| return folder; |
| } |
| |
| public File createTempFolder(String prefix) throws IORuntimeException |
| { |
| File folder = TMPUtil.createTempFolder(prefix); |
| addFileToDelete(folder); |
| return folder; |
| } |
| |
| public File createTempFolder(String prefix, String suffix) throws IORuntimeException |
| { |
| File folder = TMPUtil.createTempFolder(prefix, suffix); |
| addFileToDelete(folder); |
| return folder; |
| } |
| |
| public File createTempFolder(String prefix, String suffix, File directory) throws IORuntimeException |
| { |
| File folder = TMPUtil.createTempFile(prefix, suffix, directory); |
| addFileToDelete(folder); |
| return folder; |
| } |
| |
| public File createTempFile() throws IORuntimeException |
| { |
| File file = TMPUtil.createTempFile(); |
| addFileToDelete(file); |
| return file; |
| } |
| |
| public File createTempFile(String prefix) throws IORuntimeException |
| { |
| File file = TMPUtil.createTempFile(prefix); |
| addFileToDelete(file); |
| return file; |
| } |
| |
| public File createTempFile(String prefix, String suffix) throws IORuntimeException |
| { |
| File file = TMPUtil.createTempFile(prefix, suffix); |
| addFileToDelete(file); |
| return file; |
| } |
| |
| public File createTempFile(String prefix, String suffix, File directory) throws IORuntimeException |
| { |
| File file = TMPUtil.createTempFile(prefix, suffix, directory); |
| addFileToDelete(file); |
| return file; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return getClass().getSimpleName() + "." + getName(); |
| } |
| |
| public static AbstractOMTest getCurrrentTest() |
| { |
| return (AbstractOMTest)TestExecuter.getValue(); |
| } |
| |
| /** |
| * @deprecated Use assertEquals(message, true, ...) |
| */ |
| @Deprecated |
| public static void assertTrue(String message, boolean condition) |
| { |
| throw new UnsupportedOperationException("Use assertEquals(message, true, ...)"); |
| } |
| |
| /** |
| * @deprecated Use assertEquals(true, ...) |
| */ |
| @Deprecated |
| public static void assertTrue(boolean condition) |
| { |
| throw new UnsupportedOperationException("Use assertEquals(true, ...)"); |
| } |
| |
| /** |
| * @deprecated Use assertEquals(message, false, ...) |
| */ |
| @Deprecated |
| public static void assertFalse(String message, boolean condition) |
| { |
| throw new UnsupportedOperationException("Use assertEquals(message, false, ...)"); |
| } |
| |
| /** |
| * @deprecated Use assertEquals(false, ...) |
| */ |
| @Deprecated |
| public static void assertFalse(boolean condition) |
| { |
| throw new UnsupportedOperationException("Use assertEquals(false, ...)"); |
| } |
| |
| public static void assertEquals(Object[] expected, Object[] actual) |
| { |
| if (!Arrays.deepEquals(expected, actual)) |
| { |
| throw new AssertionFailedError( |
| "expected:" + Arrays.deepToString(expected) + " but was:" + Arrays.deepToString(actual)); |
| } |
| } |
| |
| public static void assertEquals(Object expected, Object actual) |
| { |
| if (actual == expected) |
| { |
| return; |
| } |
| |
| try |
| { |
| Assert.assertEquals(expected, actual); |
| } |
| catch (AssertionError ex) |
| { |
| AssertionFailedError error = new AssertionFailedError(ex.getMessage()); |
| error.initCause(ex); |
| throw error; |
| } |
| } |
| |
| public static void assertEquals(String message, Object expected, Object actual) |
| { |
| if (expected == null && actual == null) |
| { |
| return; |
| } |
| |
| if (expected != null && expected.equals(actual)) |
| { |
| return; |
| } |
| |
| // IMPORTANT: Give possible CDOLegacyWrapper a chance for actual, too |
| if (actual != null && actual.equals(expected)) |
| { |
| return; |
| } |
| |
| failNotEquals(message, expected, actual); |
| } |
| |
| public static void sleep(long millis) |
| { |
| msg("Sleeping " + millis); |
| ConcurrencyUtil.sleep(millis); |
| } |
| |
| public static void assertInstanceOf(Class<?> expected, Object object) |
| { |
| assertEquals("Not an instance of " + expected + ": " + object.getClass().getName(), true, |
| expected.isInstance(object)); |
| } |
| |
| public static void assertNotInstanceOf(Class<?> expected, Object object) |
| { |
| assertEquals("An instance of " + expected + ": " + object.getClass().getName(), false, expected.isInstance(object)); |
| } |
| |
| public static void assertActive(Object object) throws InterruptedException |
| { |
| final LatchTimeOuter timeOuter = new LatchTimeOuter(); |
| IListener listener = new LifecycleEventAdapter() |
| { |
| @Override |
| protected void onActivated(ILifecycle lifecycle) |
| { |
| timeOuter.countDown(); |
| } |
| }; |
| |
| EventUtil.addListener(object, listener); |
| |
| try |
| { |
| if (LifecycleUtil.isActive(object)) |
| { |
| timeOuter.countDown(); |
| } |
| |
| timeOuter.assertNoTimeOut(); |
| } |
| finally |
| { |
| EventUtil.removeListener(object, listener); |
| } |
| } |
| |
| public static void assertInactive(Object object) throws InterruptedException |
| { |
| final LatchTimeOuter timeOuter = new LatchTimeOuter(); |
| IListener listener = new LifecycleEventAdapter() |
| { |
| @Override |
| protected void onDeactivated(ILifecycle lifecycle) |
| { |
| timeOuter.countDown(); |
| } |
| }; |
| |
| EventUtil.addListener(object, listener); |
| |
| try |
| { |
| if (!LifecycleUtil.isActive(object)) |
| { |
| timeOuter.countDown(); |
| } |
| |
| timeOuter.assertNoTimeOut(); |
| } |
| finally |
| { |
| EventUtil.removeListener(object, listener); |
| } |
| } |
| |
| public static void assertSimilar(double expected, double actual, int precision) |
| { |
| final double factor = 10 * precision; |
| if (Math.round(expected * factor) != Math.round(actual * factor)) |
| { |
| assertEquals(expected, actual); |
| } |
| } |
| |
| public static void assertSimilar(float expected, float actual, int precision) |
| { |
| final float factor = 10 * precision; |
| if (Math.round(expected * factor) != Math.round(actual * factor)) |
| { |
| assertEquals(expected, actual); |
| } |
| } |
| |
| public static void msg(Object m) |
| { |
| if (!SUPPRESS_OUTPUT) |
| { |
| if (consoleEnabled && TRACER.isEnabled()) |
| { |
| TRACER.trace("--> " + m); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| public static void dumpEvents(Object notifier) |
| { |
| dumpEvents(notifier, true); |
| } |
| |
| public static void dumpEvents(Object notifier, boolean on) |
| { |
| if (notifier instanceof INotifier) |
| { |
| INotifier iNotifier = (INotifier)notifier; |
| IListener[] listeners = iNotifier.getListeners(); |
| |
| boolean wasOn = false; |
| for (int i = 0; i < listeners.length; i++) |
| { |
| if (listeners[i] == DUMPER) |
| { |
| wasOn = true; |
| break; |
| } |
| } |
| |
| if (on && !wasOn) |
| { |
| iNotifier.addListener(DUMPER); |
| } |
| else if (!on && wasOn) |
| { |
| iNotifier.removeListener(DUMPER); |
| } |
| } |
| } |
| |
| public static void skipTest(boolean skip) |
| { |
| if (skip) |
| { |
| throw new SkipTestException(); |
| } |
| } |
| |
| public static void skipTest() |
| { |
| skipTest(true); |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class SkipTestException extends RuntimeException |
| { |
| private static final long serialVersionUID = 1L; |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public static class AsyncResult<T> |
| { |
| private volatile T value; |
| |
| private CountDownLatch latch = new CountDownLatch(1); |
| |
| public AsyncResult() |
| { |
| } |
| |
| public void setValue(T value) |
| { |
| this.value = value; |
| latch.countDown(); |
| } |
| |
| public T getValue(long timeout) throws Exception |
| { |
| if (!latch.await(timeout, TimeUnit.MILLISECONDS)) |
| { |
| throw new TimeoutException("Result value not available after " + timeout + " milli seconds"); |
| } |
| |
| return value; |
| } |
| |
| public T getValue() throws Exception |
| { |
| return getValue(DEFAULT_TIMEOUT); |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public static interface ITimeOuter |
| { |
| public boolean timedOut(long timeoutMillis) throws InterruptedException; |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public static abstract class TimeOuter implements ITimeOuter |
| { |
| public boolean timedOut() throws InterruptedException |
| { |
| return timedOut(DEFAULT_TIMEOUT); |
| } |
| |
| public void assertTimeOut(long timeoutMillis) throws InterruptedException |
| { |
| assertEquals("Timeout expected", true, timedOut(timeoutMillis)); |
| } |
| |
| public void assertTimeOut() throws InterruptedException |
| { |
| assertTimeOut(DEFAULT_TIMEOUT_EXPECTED); |
| } |
| |
| public void assertNoTimeOut(long timeoutMillis) throws InterruptedException |
| { |
| assertEquals("Timeout after " + timeoutMillis + " millis", false, timedOut(timeoutMillis)); |
| } |
| |
| public void assertNoTimeOut() throws InterruptedException |
| { |
| assertNoTimeOut(DEFAULT_TIMEOUT); |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public static abstract class PollingTimeOuter extends TimeOuter |
| { |
| public static final long DEFAULT_SLEEP_MILLIS = 1; |
| |
| private long sleepMillis = DEFAULT_SLEEP_MILLIS; |
| |
| public PollingTimeOuter(long sleepMillis) |
| { |
| this.sleepMillis = sleepMillis; |
| } |
| |
| public PollingTimeOuter() |
| { |
| } |
| |
| public boolean timedOut(long timeoutMillis) throws InterruptedException |
| { |
| int retries = (int)Math.round(timeoutMillis / sleepMillis + .5d); |
| for (int i = 0; i < retries; i++) |
| { |
| if (successful()) |
| { |
| return false; |
| } |
| |
| sleep(sleepMillis); |
| } |
| |
| return true; |
| } |
| |
| protected abstract boolean successful(); |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public static class LockTimeOuter extends TimeOuter |
| { |
| private Lock lock; |
| |
| public LockTimeOuter(Lock lock) |
| { |
| this.lock = lock; |
| } |
| |
| public Lock getLock() |
| { |
| return lock; |
| } |
| |
| public boolean timedOut(long timeoutMillis) throws InterruptedException |
| { |
| Condition condition = lock.newCondition(); |
| return !condition.await(timeoutMillis, TimeUnit.MILLISECONDS); |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| public static class LatchTimeOuter extends TimeOuter |
| { |
| private CountDownLatch latch; |
| |
| public LatchTimeOuter(CountDownLatch latch) |
| { |
| this.latch = latch; |
| } |
| |
| public LatchTimeOuter(int count) |
| { |
| this(new CountDownLatch(count)); |
| } |
| |
| public LatchTimeOuter() |
| { |
| this(1); |
| } |
| |
| public CountDownLatch getLatch() |
| { |
| return latch; |
| } |
| |
| public long getCount() |
| { |
| return latch.getCount(); |
| } |
| |
| public void countDown() |
| { |
| latch.countDown(); |
| } |
| |
| public void countDown(int n) |
| { |
| for (int i = 0; i < n; i++) |
| { |
| countDown(); |
| } |
| } |
| |
| public boolean timedOut(long timeoutMillis) throws InterruptedException |
| { |
| return !latch.await(timeoutMillis, TimeUnit.MILLISECONDS); |
| } |
| } |
| } |