Merge "Refactor shutdown-impl: add parameter to RPC, remove secret validation and...
authorEd Warnicke <eaw@cisco.com>
Sat, 18 Jan 2014 17:01:03 +0000 (17:01 +0000)
committerGerrit Code Review <gerrit@opendaylight.org>
Sat, 18 Jan 2014 17:01:03 +0000 (17:01 +0000)
12 files changed:
opendaylight/config/config-api/src/main/java/org/opendaylight/controller/config/api/ValidationException.java
opendaylight/config/config-manager/src/main/java/org/opendaylight/controller/config/manager/impl/osgi/BlankTransactionServiceTracker.java
opendaylight/config/config-manager/src/main/java/org/opendaylight/controller/config/manager/impl/osgi/ConfigManagerActivator.java
opendaylight/config/config-manager/src/main/java/org/opendaylight/controller/config/manager/impl/osgi/ExtenderBundleTracker.java
opendaylight/config/shutdown-api/src/main/java/org/opendaylight/controller/config/shutdown/ShutdownService.java
opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownModule.java
opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownModuleFactory.java
opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownServiceImpl.java
opendaylight/config/shutdown-impl/src/main/yang/shutdown-impl.yang
opendaylight/config/shutdown-impl/src/test/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownTest.java
opendaylight/netconf/config-netconf-connector/src/main/java/org/opendaylight/controller/netconf/confignetconfconnector/mapping/attributes/fromxml/SimpleAttributeReadingStrategy.java
opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/xml/XmlElement.java

index da54a8341a8b477a9ebf4ba8f7cc02238668297f..f27d12399528c38c144fc17ea0354c243365a1a6 100644 (file)
@@ -140,8 +140,7 @@ public class ValidationException extends RuntimeException {
 
         @Override
         public String toString() {
-            return "ExceptionMessageWithStackTrace [message=" + message
-                    + ", stackTrace=" + stackTrace + "]";
+            return message;
         }
 
     }
index ec1a290cd204dedaf2c744dbcc97dc4cfcb36ce5..b973b9272104e22091153cb2721eab891c4b5107 100644 (file)
@@ -38,7 +38,7 @@ public class BlankTransactionServiceTracker implements ServiceTrackerCustomizer<
         return null;
     }
 
-    private synchronized void blankTransaction() {
+    synchronized void blankTransaction() {
         // race condition check: config-persister might push new configuration while server is starting up.
         ConflictingVersionException lastException = null;
         for (int i = 0; i < 10; i++) {
index 1ee6cca7e22f86b1eb634b3c94c71ee503cd1a84..be602e5b2a31fb07f90f837fc81ff63d8d16ebc4 100644 (file)
@@ -46,11 +46,11 @@ public class ConfigManagerActivator implements BundleActivator {
         configRegistryJMXRegistrator.registerToJMX(configRegistry);
 
         // track bundles containing factories
-        extenderBundleTracker = new ExtenderBundleTracker(context);
+        BlankTransactionServiceTracker blankTransactionServiceTracker = new BlankTransactionServiceTracker(configRegistry);
+        extenderBundleTracker = new ExtenderBundleTracker(context, blankTransactionServiceTracker);
         extenderBundleTracker.open();
 
-        BlankTransactionServiceTracker customizer = new BlankTransactionServiceTracker(configRegistry);
-        ServiceTracker<?, ?> serviceTracker = new ServiceTracker(context, ModuleFactory.class, customizer);
+        ServiceTracker<?, ?> serviceTracker = new ServiceTracker(context, ModuleFactory.class, blankTransactionServiceTracker);
         serviceTracker.open();
     }
 
index 6663cddacfbbcbd6e143c1c5a488720a7f4d158d..b55f3135d26cfbf1d583c0d7259905b6e9380d31 100644 (file)
@@ -34,11 +34,12 @@ import org.slf4j.LoggerFactory;
  * Code based on http://www.toedter.com/blog/?p=236
  */
 public class ExtenderBundleTracker extends BundleTracker<Object> {
-
+    private final BlankTransactionServiceTracker blankTransactionServiceTracker;
     private static final Logger logger = LoggerFactory.getLogger(ExtenderBundleTracker.class);
 
-    public ExtenderBundleTracker(BundleContext context) {
+    public ExtenderBundleTracker(BundleContext context, BlankTransactionServiceTracker blankTransactionServiceTracker) {
         super(context, Bundle.ACTIVE, null);
+        this.blankTransactionServiceTracker = blankTransactionServiceTracker;
         logger.trace("Registered as extender with context {}", context);
     }
 
@@ -64,6 +65,8 @@ public class ExtenderBundleTracker extends BundleTracker<Object> {
     @Override
     public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
         super.removedBundle(bundle,event,object);
+        // workaround for service tracker not getting removed service event
+        blankTransactionServiceTracker.blankTransaction();
     }
 
     // TODO:test
index 104241757f9979cd50ec3b0b92f8139595b1ce74..5cbe4931ef2fc005b805f0d394a2b0631ca6832c 100644 (file)
@@ -17,5 +17,5 @@ public interface ShutdownService {
      * @param inputSecret must match configured secret of the implementation
      * @param reason Optional string to be logged while shutting down
      */
-    void shutdown(String inputSecret, Optional<String> reason);
+    void shutdown(String inputSecret, Long maxWaitTime, Optional<String> reason);
 }
index f6937f9ef1f976afd652d8b630cc53eff93ffa73..d58ebf2895c327e2c55a4cc2f8ae2384c272edd8 100644 (file)
@@ -16,13 +16,11 @@ import org.osgi.framework.Bundle;
 
 public final class ShutdownModule extends AbstractShutdownModule {
     private final Bundle systemBundle;
-    private final ShutdownModule nullableOldModule;
 
     public ShutdownModule(ModuleIdentifier identifier, Bundle systemBundle) {
         super(identifier, null);
         singletonCheck(identifier);
         this.systemBundle = systemBundle;
-        this.nullableOldModule = null;
     }
 
     public ShutdownModule(ModuleIdentifier identifier, ShutdownModule oldModule, java.lang.AutoCloseable oldInstance,
@@ -30,7 +28,6 @@ public final class ShutdownModule extends AbstractShutdownModule {
         super(identifier, null, oldModule, oldInstance);
         singletonCheck(identifier);
         this.systemBundle = systemBundle;
-        this.nullableOldModule = oldModule;
     }
 
     private static void singletonCheck(ModuleIdentifier identifier) {
@@ -52,40 +49,13 @@ public final class ShutdownModule extends AbstractShutdownModule {
         throw new UnsupportedOperationException();
     }
 
-    @Override
-    public String getSecret() {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getOldSecret() {
-        throw new UnsupportedOperationException();
-    }
-
-    String getActualSecret() {
-        return super.getSecret();
-    }
-
-    String getActualOldSecret() {
-        return super.getOldSecret();
-    }
-
     @Override
     protected void customValidation() {
-        JmxAttributeValidationException.checkNotNull(super.getOldSecret(), oldSecretJmxAttribute);
         JmxAttributeValidationException.checkNotNull(super.getSecret(), secretJmxAttribute);
-        if (nullableOldModule != null) {
-            // if nothing changed, remain valid
-            boolean sameAsOldModule = isSame(nullableOldModule);
-            if (sameAsOldModule == false) {
-                boolean valid = getActualOldSecret().equals(nullableOldModule.getActualSecret());
-                JmxAttributeValidationException.checkCondition(valid, "Invalid old secret", oldSecretJmxAttribute);
-            }
-        }
     }
 
     @Override
     public java.lang.AutoCloseable createInstance() {
-        return new ShutdownServiceImpl(getActualSecret(), systemBundle, getRootRuntimeBeanRegistratorWrapper());
+        return new ShutdownServiceImpl(getSecret(), systemBundle, getRootRuntimeBeanRegistratorWrapper());
     }
 }
index 637395bc48f334eda60084f4404f94577d1c2ac7..0517fafd56ac46662ba1d2e98f6594841ba89aad 100644 (file)
 package org.opendaylight.controller.config.yang.shutdown.impl;
 
 import org.opendaylight.controller.config.api.DependencyResolver;
+import org.opendaylight.controller.config.api.DependencyResolverFactory;
 import org.opendaylight.controller.config.api.ModuleIdentifier;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 
-public class ShutdownModuleFactory extends AbstractShutdownModuleFactory {
-
-    @Override
-    public org.opendaylight.controller.config.spi.Module createModule(String instanceName, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, org.opendaylight.controller.config.api.DynamicMBeanWithInstance old, org.osgi.framework.BundleContext bundleContext) throws Exception {
-        org.opendaylight.controller.config.yang.shutdown.impl.ShutdownModule oldModule = null;
-        try {
-            oldModule = (org.opendaylight.controller.config.yang.shutdown.impl.ShutdownModule) old.getModule();
-        } catch(Exception e) {
-            return handleChangedClass(old);
-        }
-        org.opendaylight.controller.config.yang.shutdown.impl.ShutdownModule module = instantiateModule(instanceName, dependencyResolver, oldModule, old.getInstance(), bundleContext);
-
-        module.setOldSecret(oldModule.getActualOldSecret());
-        module.setSecret(oldModule.getActualSecret());
-
-        return module;
-    }
+import java.util.Arrays;
+import java.util.Set;
 
+public class ShutdownModuleFactory extends AbstractShutdownModuleFactory {
 
     public ShutdownModule instantiateModule(String instanceName, DependencyResolver dependencyResolver,
                                             ShutdownModule oldModule, AutoCloseable oldInstance,
@@ -46,4 +33,12 @@ public class ShutdownModuleFactory extends AbstractShutdownModuleFactory {
         Bundle systemBundle = bundleContext.getBundle(0);
         return new ShutdownModule(new ModuleIdentifier(NAME, instanceName), systemBundle);
     }
+
+    @Override
+    public Set<ShutdownModule> getDefaultModules(DependencyResolverFactory dependencyResolverFactory, BundleContext bundleContext) {
+        ModuleIdentifier id = new ModuleIdentifier(NAME, NAME);
+        DependencyResolver dependencyResolver = dependencyResolverFactory.createDependencyResolver(id);
+        ShutdownModule shutdownModule = instantiateModule(NAME, dependencyResolver, bundleContext);
+        return new java.util.HashSet<>(Arrays.asList(shutdownModule));
+    }
 }
index e5b95c812bd63545289d536d7a12894b6b217c95..6cdcf605b04716537d73252d75443e92506b3a14 100644 (file)
@@ -14,6 +14,9 @@ import org.osgi.framework.BundleException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+
 public class ShutdownServiceImpl implements ShutdownService, AutoCloseable {
     private final ShutdownService impl;
     private final ShutdownRuntimeRegistration registration;
@@ -28,8 +31,8 @@ public class ShutdownServiceImpl implements ShutdownService, AutoCloseable {
     }
 
     @Override
-    public void shutdown(String inputSecret, Optional<String> reason) {
-        impl.shutdown(inputSecret, reason);
+    public void shutdown(String inputSecret, Long maxWaitTime, Optional<String> reason) {
+        impl.shutdown(inputSecret, maxWaitTime, reason);
     }
 
     @Override
@@ -49,7 +52,7 @@ class Impl implements ShutdownService {
     }
 
     @Override
-    public void shutdown(String inputSecret, Optional<String> reason) {
+    public void shutdown(String inputSecret, Long maxWaitTime, Optional<String> reason) {
         logger.warn("Shutdown issued with secret {} and reason {}", inputSecret, reason);
         try {
             Thread.sleep(1000); // prevent brute force attack
@@ -60,22 +63,15 @@ class Impl implements ShutdownService {
         if (this.secret.equals(inputSecret)) {
             logger.info("Server is shutting down");
 
-            Thread stopSystemBundle = new Thread() {
-                @Override
-                public void run() {
-                    try {
-                        // wait so that JMX response is received
-                        Thread.sleep(1000);
-                        systemBundle.stop();
-                    } catch (BundleException e) {
-                        logger.warn("Can not stop OSGi server", e);
-                    } catch (InterruptedException e) {
-                        logger.warn("Shutdown process interrupted", e);
-                    }
-                }
-            };
-            stopSystemBundle.start();
-
+            // actual work:
+            Thread stopSystemBundleThread = new StopSystemBundleThread(systemBundle);
+            stopSystemBundleThread.start();
+            if (maxWaitTime != null && maxWaitTime > 0) {
+                Thread systemExitThread = new CallSystemExitThread(maxWaitTime);
+                logger.debug("Scheduling {}", systemExitThread);
+                systemExitThread.start();
+            }
+            // end
         } else {
             logger.warn("Unauthorized attempt to shut down server");
             throw new IllegalArgumentException("Invalid secret");
@@ -84,6 +80,70 @@ class Impl implements ShutdownService {
 
 }
 
+class StopSystemBundleThread extends Thread {
+    private static final Logger logger = LoggerFactory.getLogger(StopSystemBundleThread.class);
+    private final Bundle systemBundle;
+
+    StopSystemBundleThread(Bundle systemBundle) {
+        super("stop-system-bundle");
+        this.systemBundle = systemBundle;
+    }
+
+    @Override
+    public void run() {
+        try {
+            // wait so that JMX response is received
+            Thread.sleep(1000);
+            systemBundle.stop();
+        } catch (BundleException e) {
+            logger.warn("Can not stop OSGi server", e);
+        } catch (InterruptedException e) {
+            logger.warn("Shutdown process interrupted", e);
+        }
+    }
+}
+
+class CallSystemExitThread extends Thread {
+    private static final Logger logger = LoggerFactory.getLogger(CallSystemExitThread.class);
+    private final long maxWaitTime;
+    CallSystemExitThread(long maxWaitTime) {
+        super("call-system-exit-daemon");
+        setDaemon(true);
+        if (maxWaitTime <= 0){
+            throw new IllegalArgumentException("Cannot schedule to zero or negative time:" + maxWaitTime);
+        }
+        this.maxWaitTime = maxWaitTime;
+    }
+
+    @Override
+    public String toString() {
+        return "CallSystemExitThread{" +
+                "maxWaitTime=" + maxWaitTime +
+                '}';
+    }
+
+    @Override
+    public void run() {
+        try {
+            // wait specified time
+            Thread.sleep(maxWaitTime);
+            logger.error("Since some threads are still running, server is going to shut down via System.exit(1) !");
+            // do a thread dump
+            ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);
+            StringBuffer sb = new StringBuffer();
+            for(ThreadInfo info : threads) {
+                sb.append(info);
+                sb.append("\n");
+            }
+            logger.warn("Thread dump:{}", sb);
+            System.exit(1);
+        } catch (InterruptedException e) {
+            logger.info("Interrupted, not going to call System.exit(1)");
+        }
+    }
+}
+
+
 class MXBeanImpl implements ShutdownRuntimeMXBean {
     private final ShutdownService impl;
 
@@ -92,13 +152,13 @@ class MXBeanImpl implements ShutdownRuntimeMXBean {
     }
 
     @Override
-    public void shutdown(String inputSecret, String nullableReason) {
+    public void shutdown(String inputSecret, Long maxWaitTime, String nullableReason) {
         Optional<String> optionalReason;
         if (nullableReason == null) {
             optionalReason = Optional.absent();
         } else {
             optionalReason = Optional.of(nullableReason);
         }
-        impl.shutdown(inputSecret, optionalReason);
+        impl.shutdown(inputSecret, maxWaitTime, optionalReason);
     }
 }
index 78b44abfb21666882f87df88b96fd946bf26ef22..883735c0c8d495c53c0eb2cc12db547507805ac3 100644 (file)
@@ -37,10 +37,6 @@ module shutdown-impl {
                 type string;
                 default "";
             }
-            leaf old-secret {
-                type string;
-                default "";
-            }
         }
     }
 
@@ -63,6 +59,10 @@ module shutdown-impl {
             leaf input-secret {
                 type string;
             }
+            leaf max-wait-time {
+                type uint32;
+                description "Maximum time in milliseconds before process is forcibly exited. Zero or null cancels this functionality.";
+            }
             leaf reason {
                 type string;
             }
index 86cd6fa812fabb9dcd671afe9baa2bca714554de..5887e98f30daa4816da2bfb1e1215a6385ebc783 100644 (file)
@@ -11,8 +11,6 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.opendaylight.controller.config.api.ValidationException;
-import org.opendaylight.controller.config.api.ValidationException.ExceptionMessageWithStackTrace;
 import org.opendaylight.controller.config.api.jmx.ObjectNameUtil;
 import org.opendaylight.controller.config.manager.impl.AbstractConfigTest;
 import org.opendaylight.controller.config.manager.impl.factoriesresolver.HardcodedModuleFactoriesResolver;
@@ -23,11 +21,8 @@ import org.osgi.framework.Bundle;
 import javax.management.JMX;
 import javax.management.ObjectName;
 import java.util.Collections;
-import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -49,6 +44,9 @@ public class ShutdownTest extends AbstractConfigTest {
         doReturn(mockedSysBundle).when(mockedContext).getBundle(0);
         mockedContext.getBundle(0);
         doNothing().when(mockedSysBundle).stop();
+        ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction();
+        // initialize default instance
+        transaction.commit();
     }
 
     @Test
@@ -62,21 +60,20 @@ public class ShutdownTest extends AbstractConfigTest {
         }
     }
 
+    private static final ObjectName runtimeON = ObjectNameUtil.createRuntimeBeanName(NAME, NAME, Collections.<String, String>emptyMap());
+
     @Test
     public void testWithoutSecret() throws Exception {
-        ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction();
-        transaction.createModule(NAME, NAME);
-        transaction.commit();
         // test JMX rpc
-        ObjectName runtimeON = ObjectNameUtil.createRuntimeBeanName(NAME, NAME, Collections.<String, String>emptyMap());
+
         ShutdownRuntimeMXBean runtime = configRegistryClient.newMXBeanProxy(runtimeON, ShutdownRuntimeMXBean.class);
         try {
-            runtime.shutdown("foo", null);
+            runtime.shutdown("foo", 60000L, null);
             fail();
         } catch (IllegalArgumentException e) {
             assertEquals("Invalid secret", e.getMessage());
         }
-        runtime.shutdown("", null);
+        runtime.shutdown("", 60000L, null);
         assertStopped();
     }
 
@@ -84,57 +81,31 @@ public class ShutdownTest extends AbstractConfigTest {
     @Test
     public void testWithSecret() throws Exception {
         ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction();
-        ObjectName on = transaction.createModule(NAME, NAME);
+        ObjectName on = transaction.lookupConfigBean(NAME, NAME);
         ShutdownModuleMXBean proxy = transaction.newMXBeanProxy(on, ShutdownModuleMXBean.class);
         String secret = "secret";
         proxy.setSecret(secret);
         transaction.commit();
         shutdownViaRuntimeJMX(secret);
-
-        // test old secret
-        transaction = configRegistryClient.createTransaction();
-        on = transaction.lookupConfigBean(NAME, NAME);
-        proxy = transaction.newMXBeanProxy(on, ShutdownModuleMXBean.class);
-        try {
-            rethrowCause(proxy).getOldSecret();
-            fail();
-        } catch (UnsupportedOperationException e) {
-        }
         try {
-            rethrowCause(proxy).getSecret();
+            ShutdownRuntimeMXBean runtime = JMX.newMXBeanProxy(platformMBeanServer, runtimeON, ShutdownRuntimeMXBean.class);
+            runtime.shutdown("foo", 60000L, null);
             fail();
-        } catch (UnsupportedOperationException e) {
-        }
-        // set secret to nothing
-        String newSecret = "newSecret";
-        proxy.setSecret(newSecret);
-        try {
-            transaction.commit();
-            fail("Old secret not provided - should fail validation");
-        } catch (ValidationException e) {
-            Map<String, Map<String, ExceptionMessageWithStackTrace>> failedValidations = e.getFailedValidations();
-            assertTrue(failedValidations.containsKey(NAME));
-            ExceptionMessageWithStackTrace exceptionMessageWithStackTrace = failedValidations.get(NAME).get(NAME);
-            assertNotNull(exceptionMessageWithStackTrace);
-            assertEquals("OldSecret Invalid old secret", exceptionMessageWithStackTrace.getMessage());
-
+        } catch (IllegalArgumentException e) {
+            assertEquals("Invalid secret", e.getMessage());
         }
-        proxy.setOldSecret(secret);
-        transaction.commit();
-        shutdownViaRuntimeJMX(newSecret);
     }
 
     private void shutdownViaRuntimeJMX(String secret) throws Exception {
         // test JMX rpc
-        ObjectName runtimeON = ObjectNameUtil.createRuntimeBeanName(NAME, NAME, Collections.<String, String>emptyMap());
         ShutdownRuntimeMXBean runtime = JMX.newMXBeanProxy(platformMBeanServer, runtimeON, ShutdownRuntimeMXBean.class);
         try {
-            runtime.shutdown("", null);
+            runtime.shutdown("", 60000L, null);
             fail();
         } catch (IllegalArgumentException e) {
             assertEquals("Invalid secret", e.getMessage());
         }
-        runtime.shutdown(secret, null);
+        runtime.shutdown(secret, 60000L, null);
         assertStopped();
     }
 
index a8605243af3f77f587563d453ea69d51401f2aa4..625e4ab3dfe9f8c4a7aeb28879eb8daad31e1bfa 100644 (file)
@@ -10,10 +10,14 @@ package org.opendaylight.controller.netconf.confignetconfconnector.mapping.attri
 
 import com.google.common.base.Preconditions;
 import org.opendaylight.controller.netconf.util.xml.XmlElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.List;
 
 public class SimpleAttributeReadingStrategy extends AbstractAttributeReadingStrategy {
+    private static final Logger logger = LoggerFactory.getLogger(SimpleAttributeReadingStrategy.class);
+
 
     public SimpleAttributeReadingStrategy(String nullableDefault) {
         super(nullableDefault);
@@ -25,7 +29,13 @@ public class SimpleAttributeReadingStrategy extends AbstractAttributeReadingStra
         Preconditions.checkState(configNodes.size() == 1, "This element should be present only once " + xmlElement
                 + " but was " + configNodes.size());
 
-        String textContent = xmlElement.getTextContent();
+        String textContent = "";
+        try{
+            textContent = xmlElement.getTextContent();
+        }catch(IllegalStateException | NullPointerException e) {
+            // yuma sends <attribute /> for empty value instead of <attribute></attribute>
+            logger.warn("Ignoring exception caused by failure to read text element", e);
+        }
 
         Preconditions.checkNotNull(textContent, "This element should contain text %s", xmlElement);
         return AttributeConfigElement.create(postprocessNullableDefault(getNullableDefault()),
index 212214cd2ab516e8f7c8e1226b63bb16d886e927..18a94c6d07ff7cf74289bfb5d3e057967a61e86c 100644 (file)
@@ -272,7 +272,9 @@ public class XmlElement {
 
     public String getTextContent() {
         Node textChild = element.getFirstChild();
-        Preconditions.checkState(textChild instanceof Text, getName() + " should contain text");
+        Preconditions.checkNotNull(textChild, "Child node expected, got null for " + getName() + " : " + element);
+        Preconditions.checkState(textChild instanceof Text, getName() + " should contain text." +
+                Text.class.getName() + " expected, got " + textChild);
         String content = textChild.getTextContent();
         // Trim needed
         return content.trim();