/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ambari.server.controller.logging; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import org.apache.ambari.server.configuration.Configuration; import org.apache.ambari.server.controller.AmbariManagementController; import org.easymock.Capture; import org.easymock.EasyMock; import org.easymock.EasyMockSupport; import org.junit.Test; import com.google.common.cache.Cache; import com.google.inject.Injector; /** * This test verifies the basic behavior of the * LogSearchDataRetrievalServiceTest, and should * verify the interaction with its dependencies, as * well as the interaction with the LogSearch * server. * */ public class LogSearchDataRetrievalServiceTest { @Test public void testGetTailFileWhenHelperIsAvailable() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; final String expectedResultURI = "http://localhost/test/result"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); LoggingRequestHelper helperMock = mockSupport.createMock(LoggingRequestHelper.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); expect(helperFactoryMock.getHelper(null, expectedClusterName)).andReturn(helperMock); expect(helperMock.createLogFileTailURI("http://localhost", expectedComponentName, expectedHostName)).andReturn(expectedResultURI); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); String resultTailFileURI = retrievalService.getLogFileTailURI("http://localhost", expectedComponentName, expectedHostName, expectedClusterName); assertEquals("TailFileURI was not returned as expected", expectedResultURI, resultTailFileURI); mockSupport.verifyAll(); } @Test public void testGetTailFileWhenRequestHelperIsNull() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); // return null, to simulate the case where LogSearch Server is // not available for some reason expect(helperFactoryMock.getHelper(null, expectedClusterName)).andReturn(null); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); String resultTailFileURI = retrievalService.getLogFileTailURI("http://localhost", expectedComponentName, expectedHostName, expectedClusterName); assertNull("TailFileURI should be null in this case", resultTailFileURI); mockSupport.verifyAll(); } @Test public void testGetLogFileNamesDefault() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); Executor executorMock = mockSupport.createMock(Executor.class); Injector injectorMock = mockSupport.createMock(Injector.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); // expect the executor to be called to execute the LogSearch request executorMock.execute(isA(LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable.class)); // executor should only be called once expectLastCall().once(); expect(injectorMock.getInstance(LoggingRequestHelperFactory.class)).andReturn(helperFactoryMock); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setInjector(injectorMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); retrievalService.setExecutor(executorMock); assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size()); Set<String> resultSet = retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName); assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet); assertEquals("Incorrect number of entries in the current request set", 1, retrievalService.getCurrentRequests().size()); assertTrue("Incorrect HostComponent set on request set", retrievalService.getCurrentRequests().contains(expectedComponentName + "+" + expectedHostName)); assertEquals("Incorrect size for failure counts for components, should be 0", 0, retrievalService.getComponentRequestFailureCounts().size()); mockSupport.verifyAll(); } @Test public void testGetLogFileNamesExistingFailuresLessThanThreshold() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); Executor executorMock = mockSupport.createMock(Executor.class); Injector injectorMock = mockSupport.createMock(Injector.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); // expect the executor to be called to execute the LogSearch request executorMock.execute(isA(LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable.class)); // executor should only be called once expectLastCall().once(); expect(injectorMock.getInstance(LoggingRequestHelperFactory.class)).andReturn(helperFactoryMock); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setInjector(injectorMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); retrievalService.setExecutor(executorMock); // initialize the comopnent-based failure count to have a count > 0, but less than threshold (10) retrievalService.getComponentRequestFailureCounts().put(expectedComponentName, new AtomicInteger(5)); assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size()); Set<String> resultSet = retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName); assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet); assertEquals("Incorrect number of entries in the current request set", 1, retrievalService.getCurrentRequests().size()); assertTrue("Incorrect HostComponent set on request set", retrievalService.getCurrentRequests().contains(expectedComponentName + "+" + expectedHostName)); assertEquals("Incorrect size for failure counts for components, should be 0", 1, retrievalService.getComponentRequestFailureCounts().size()); assertEquals("Incorrect failure count for component", 5, retrievalService.getComponentRequestFailureCounts().get(expectedComponentName).get()); mockSupport.verifyAll(); } @Test public void testGetLogFileNamesExistingFailuresAtThreshold() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); Executor executorMock = mockSupport.createMock(Executor.class); Injector injectorMock = mockSupport.createMock(Injector.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setInjector(injectorMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); retrievalService.setExecutor(executorMock); // initialize the comopnent-based failure count to have a count at the threshold retrievalService.getComponentRequestFailureCounts().put(expectedComponentName, new AtomicInteger(10)); assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size()); Set<String> resultSet = retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName); assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet); assertEquals("Incorrect number of entries in the current request set", 0, retrievalService.getCurrentRequests().size()); assertEquals("Incorrect size for failure counts for components, should be 0", 1, retrievalService.getComponentRequestFailureCounts().size()); assertEquals("Incorrect failure count for component", 10, retrievalService.getComponentRequestFailureCounts().get(expectedComponentName).get()); mockSupport.verifyAll(); } @Test public void testGetLogFileNamesExistingFailuresOverThreshold() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); Executor executorMock = mockSupport.createMock(Executor.class); Injector injectorMock = mockSupport.createMock(Injector.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setInjector(injectorMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); retrievalService.setExecutor(executorMock); // initialize the comopnent-based failure count to have a count over the threshold retrievalService.getComponentRequestFailureCounts().put(expectedComponentName, new AtomicInteger(20)); assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size()); Set<String> resultSet = retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName); assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet); assertEquals("Incorrect number of entries in the current request set", 0, retrievalService.getCurrentRequests().size()); assertEquals("Incorrect size for failure counts for components, should be 0", 1, retrievalService.getComponentRequestFailureCounts().size()); assertEquals("Incorrect failure count for component", 20, retrievalService.getComponentRequestFailureCounts().get(expectedComponentName).get()); mockSupport.verifyAll(); } @Test public void testGetLogFileNamesIgnoreMultipleRequestsForSameHostComponent() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); Executor executorMock = mockSupport.createMock(Executor.class); Configuration configurationMock = mockSupport.createMock(Configuration.class); expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce(); mockSupport.replayAll(); LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService(); retrievalService.setLoggingRequestHelperFactory(helperFactoryMock); retrievalService.setConfiguration(configurationMock); // call the initialization routine called by the Google framework retrievalService.doStart(); // there should be no expectations set on this mock retrievalService.setExecutor(executorMock); // set the current requests to include this expected HostComponent // this simulates the case where a request is ongoing for this HostComponent, // but is not yet completed. retrievalService.getCurrentRequests().add(expectedComponentName + "+" + expectedHostName); Set<String> resultSet = retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName); assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet); mockSupport.verifyAll(); } @Test @SuppressWarnings("unchecked") public void testRunnableWithSuccessfulCall() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); AmbariManagementController controllerMock = mockSupport.createMock(AmbariManagementController.class); LoggingRequestHelper helperMock = mockSupport.createMock(LoggingRequestHelper.class); Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class); Set<String> currentRequestsMock = mockSupport.createMock(Set.class); Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class); expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock); expect(helperMock.sendGetLogFileNamesRequest(expectedComponentName, expectedHostName)).andReturn(Collections.singleton("/this/is/just/a/test/directory")); // expect that the results will be placed in the cache cacheMock.put(expectedComponentAndHostName, Collections.singleton("/this/is/just/a/test/directory")); // expect that the completed request is removed from the current request set expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once(); mockSupport.replayAll(); LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable = new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName, cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock); loggingRunnable.run(); mockSupport.verifyAll(); } @Test @SuppressWarnings("unchecked") public void testRunnableWithFailedCallNullHelper() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName; EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); AmbariManagementController controllerMock = mockSupport.createMock(AmbariManagementController.class); Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class); Set<String> currentRequestsMock = mockSupport.createMock(Set.class); Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class); // return null to simulate an error during helper instance creation expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(null); // expect that the completed request is removed from the current request set, // even in the event of a failure to obtain the LogSearch data expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once(); mockSupport.replayAll(); LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable = new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName, cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock); loggingRunnable.run(); mockSupport.verifyAll(); } @Test @SuppressWarnings("unchecked") public void testRunnableWithFailedCallNullResult() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName; final AtomicInteger testInteger = new AtomicInteger(0); EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); AmbariManagementController controllerMock = mockSupport.createMock(AmbariManagementController.class); LoggingRequestHelper helperMock = mockSupport.createMock(LoggingRequestHelper.class); Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class); Set<String> currentRequestsMock = mockSupport.createMock(Set.class); Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class); Capture<AtomicInteger> captureFailureCount = EasyMock.newCapture(); expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock); // return null to simulate an error occurring during the LogSearch data request expect(helperMock.sendGetLogFileNamesRequest(expectedComponentName, expectedHostName)).andReturn(null); // expect that the completed request is removed from the current request set, // even in the event of a failure to obtain the LogSearch data expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once(); // expect that the component failure map is initially empty expect(componentFailureCounts.containsKey(expectedComponentName)).andReturn(false); // expect that the component map is updated with a new count expect(componentFailureCounts.put(eq(expectedComponentName), capture(captureFailureCount))).andReturn(new AtomicInteger(0)); // expect that the runnable will obtain an increment the failure count expect(componentFailureCounts.get(expectedComponentName)).andReturn(testInteger); mockSupport.replayAll(); LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable = new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName, cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock); loggingRunnable.run(); assertEquals("Initial count set by Runnable should be 0", 0, captureFailureCount.getValue().get()); assertEquals("Failure count should have been incremented", 1, testInteger.get()); mockSupport.verifyAll(); } @Test @SuppressWarnings("unchecked") public void testRunnableWithFailedCallNullResultExistingFailureCount() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName; final AtomicInteger testFailureCount = new AtomicInteger(2); EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); AmbariManagementController controllerMock = mockSupport.createMock(AmbariManagementController.class); LoggingRequestHelper helperMock = mockSupport.createMock(LoggingRequestHelper.class); Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class); Set<String> currentRequestsMock = mockSupport.createMock(Set.class); Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class); expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock); // return null to simulate an error occurring during the LogSearch data request expect(helperMock.sendGetLogFileNamesRequest(expectedComponentName, expectedHostName)).andReturn(null); // expect that the completed request is removed from the current request set, // even in the event of a failure to obtain the LogSearch data expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once(); // expect that the component failure map is initially empty expect(componentFailureCounts.containsKey(expectedComponentName)).andReturn(true); // expect that the runnable will obtain an increment the existing failure count expect(componentFailureCounts.get(expectedComponentName)).andReturn(testFailureCount); mockSupport.replayAll(); assertEquals("Initial count should be 2", 2, testFailureCount.get()); LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable = new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName, cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock); loggingRunnable.run(); assertEquals("Failure count should have been incremented", 3, testFailureCount.get()); mockSupport.verifyAll(); } @Test @SuppressWarnings("unchecked") public void testRunnableWithFailedCallEmptyResult() throws Exception { final String expectedHostName = "c6401.ambari.apache.org"; final String expectedComponentName = "DATANODE"; final String expectedClusterName = "clusterone"; final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName; final AtomicInteger testInteger = new AtomicInteger(0); EasyMockSupport mockSupport = new EasyMockSupport(); LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class); AmbariManagementController controllerMock = mockSupport.createMock(AmbariManagementController.class); LoggingRequestHelper helperMock = mockSupport.createMock(LoggingRequestHelper.class); Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class); Set<String> currentRequestsMock = mockSupport.createMock(Set.class); Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class); Capture<AtomicInteger> captureFailureCount = EasyMock.newCapture(); expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock); // return null to simulate an error occurring during the LogSearch data request expect(helperMock.sendGetLogFileNamesRequest(expectedComponentName, expectedHostName)).andReturn(Collections.<String>emptySet()); // expect that the completed request is removed from the current request set, // even in the event of a failure to obtain the LogSearch data expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once(); // expect that the component failure map is initially empty expect(componentFailureCounts.containsKey(expectedComponentName)).andReturn(false); // expect that the component map is updated with a new count expect(componentFailureCounts.put(eq(expectedComponentName), capture(captureFailureCount))).andReturn(new AtomicInteger(0)); // expect that the runnable will obtain an increment the failure count expect(componentFailureCounts.get(expectedComponentName)).andReturn(testInteger); mockSupport.replayAll(); LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable = new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName, cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock); loggingRunnable.run(); assertEquals("Initial count set by Runnable should be 0", 0, captureFailureCount.getValue().get()); assertEquals("Failure count should have been incremented", 1, testInteger.get()); mockSupport.verifyAll(); } }