diff options
7 files changed, 219 insertions, 53 deletions
diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/builder/AbstractAuthenticationDataBuilder.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/builder/AbstractAuthenticationDataBuilder.java index 9d24eb8c..2c125cc0 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/builder/AbstractAuthenticationDataBuilder.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/builder/AbstractAuthenticationDataBuilder.java @@ -22,7 +22,9 @@ package at.gv.egiz.eaaf.core.impl.idp.auth.builder; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -60,6 +62,9 @@ import at.gv.egiz.eaaf.core.impl.idp.AuthenticationData; import at.gv.egiz.eaaf.core.impl.idp.auth.data.AuthProcessDataWrapper; import at.gv.egiz.eaaf.core.impl.idp.auth.data.SimpleIdentityLinkAssertionParser; import at.gv.egiz.eaaf.core.impl.utils.XPathUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; public abstract class AbstractAuthenticationDataBuilder implements IAuthenticationDataBuilder { private static final Logger log = @@ -174,7 +179,7 @@ public abstract class AbstractAuthenticationDataBuilder implements IAuthenticati // includedToGenericAuthData = // authProcessData.getGenericSessionDataStorage().keySet(); // else - initializeThreadLocalVariable(authProcessData, new HashSet<>()); + initializeThreadLocalVariable(authProcessData, new HashSet<String>()); // #################################################### // set general authData info's @@ -224,23 +229,23 @@ public abstract class AbstractAuthenticationDataBuilder implements IAuthenticati } /** - * Initialize Thread-Local holder for generic attributes set in authenticated session. + * Initialize Thread-Local holder for generic attributes set in authenticated + * session. * * @param authProcessData Current authentication data holder - * @param data {@link Collection} of generic attribute-names - * @throws EaafAuthenticationException In case of an error + * @param set Initial set of attributes of this holder + * @throws EaafAuthenticationException In case of an error */ - protected void initializeThreadLocalVariable(@NonNull final IAuthProcessDataContainer authProcessData, - Set<String> data) + protected void initializeThreadLocalVariable( + @NonNull final IAuthProcessDataContainer authProcessData, Set<String> set) throws EaafAuthenticationException { try { - authProcessData.setGenericDataToSession(GENERIC_ATTR_CONTAINER, data); + authProcessData.setGenericDataToSession(GENERIC_ATTR_CONTAINER, new AttributeListHolder(set)); } catch (EaafStorageException e) { throw new EaafAuthenticationException("builder.11", new Object[] { e.getMessage() }, e); } - } /** @@ -248,9 +253,10 @@ public abstract class AbstractAuthenticationDataBuilder implements IAuthenticati * * @param set {@link Collection} of generic attribute-names */ - @SuppressWarnings("unchecked") protected Set<String> getThreadLocalVariable(@NonNull final IAuthProcessDataContainer authProcessData) { - return authProcessData.getGenericDataFromSession(GENERIC_ATTR_CONTAINER, Set.class); + AttributeListHolder holder = authProcessData.getGenericDataFromSession(GENERIC_ATTR_CONTAINER, + AttributeListHolder.class); + return holder != null ? holder.getProcessedAttributes() : Collections.emptySet(); } @@ -370,7 +376,7 @@ public abstract class AbstractAuthenticationDataBuilder implements IAuthenticati initializeThreadLocalVariable(authProcessData, authProcessData.getGenericSessionDataStorage().keySet()); } else { - initializeThreadLocalVariable(authProcessData, new HashSet<>()); + initializeThreadLocalVariable(authProcessData, new HashSet<String>()); } // #################################################### @@ -804,4 +810,14 @@ public abstract class AbstractAuthenticationDataBuilder implements IAuthenticati } + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class AttributeListHolder implements Serializable { + private static final long serialVersionUID = -7767295315249087217L; + + private Set<String> processedAttributes; + + } + } diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/data/AuthProcessDataWrapper.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/data/AuthProcessDataWrapper.java index e76d0728..dc391902 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/data/AuthProcessDataWrapper.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/data/AuthProcessDataWrapper.java @@ -19,9 +19,9 @@ package at.gv.egiz.eaaf.core.impl.idp.auth.data; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.io.Serializable; import java.time.Instant; +import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; @@ -33,19 +33,25 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import at.gv.egiz.eaaf.core.api.data.EaafConstants; import at.gv.egiz.eaaf.core.api.idp.EaafAuthProcessDataConstants; import at.gv.egiz.eaaf.core.api.idp.auth.data.IAuthProcessDataContainer; import at.gv.egiz.eaaf.core.api.idp.auth.data.IIdentityLink; +import at.gv.egiz.eaaf.core.exceptions.EaafJsonMapperException; import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; - +import at.gv.egiz.eaaf.core.impl.utils.DefaultJsonMapper; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class AuthProcessDataWrapper implements IAuthProcessDataContainer, EaafAuthProcessDataConstants { - private static final Logger log = LoggerFactory.getLogger(AuthProcessDataWrapper.class); + public static final String GENERIC_DATA_PREFIX = "$GENERIC$_"; public static final String PATTERN_ISSUE_INSTANT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; protected Map<String, Object> authProcessData; @@ -57,7 +63,7 @@ public class AuthProcessDataWrapper @Override public Instant getIssueInstant() { - return wrapStoredObject(VALUE_ISSUEINSTANT, null, Instant.class); + return buildDateTimeUtcDate(wrapStoredObject(VALUE_ISSUEINSTANT, null, String.class)); } /* @@ -68,8 +74,7 @@ public class AuthProcessDataWrapper */ @Override public String getIssueInstantString() { - return buildDateTimeUtcString( - wrapStoredObject(VALUE_ISSUEINSTANT, null, Instant.class)); + return wrapStoredObject(VALUE_ISSUEINSTANT, null, String.class); } /* @@ -81,9 +86,13 @@ public class AuthProcessDataWrapper */ @Override public void setIssueInstant(final String issueInstant) { - authProcessData.put(VALUE_ISSUEINSTANT, - buildDateTimeUtcDate(issueInstant)); + if (buildDateTimeUtcDate(issueInstant) != null) { + authProcessData.put(VALUE_ISSUEINSTANT, issueInstant); + + } else if (StringUtils.isNotEmpty(issueInstant)) { + log.warn("Can not parse IssueInstant of authentication: {}", issueInstant); + } } /* @@ -95,7 +104,7 @@ public class AuthProcessDataWrapper */ @Override public void setIssueInstant(final Instant issueInstant) { - authProcessData.put(VALUE_ISSUEINSTANT, issueInstant); + authProcessData.put(VALUE_ISSUEINSTANT, buildDateTimeUtcString(issueInstant)); } @@ -257,8 +266,8 @@ public class AuthProcessDataWrapper * at.gv.egovernment.moa.id.auth.data.IAuthenticationSession#getSessionCreated() */ @Override - public Date getSessionCreated() { - return wrapStoredObject(EaafConstants.AUTH_DATA_CREATED, null, Date.class); + public Instant getSessionCreated() { + return buildDateTimeUtcDate(wrapStoredObject(EaafConstants.AUTH_DATA_CREATED, null, String.class)); } /* @@ -271,6 +280,15 @@ public class AuthProcessDataWrapper public Map<String, Object> getGenericSessionDataStorage() { return authProcessData.entrySet().stream() .filter(el -> el.getKey().startsWith(GENERIC_PREFIX)) + .map(el -> { + Object obj = el.getValue(); + if (obj instanceof String && ((String) obj).startsWith(GENERIC_DATA_PREFIX)) { + log.trace("Find generic JSON serialized data. Starting de-serialization ... "); + el.setValue(objectSaveJsonDeserialization(obj)); + + } + return el; + }) .collect( Collectors.toMap( el -> el.getKey().substring(GENERIC_PREFIX.length()), @@ -292,7 +310,8 @@ public class AuthProcessDataWrapper */ @Override public Object getGenericDataFromSession(final String key) { - return authProcessData.get(GENERIC_PREFIX + key); + return wrapStoredObject(GENERIC_PREFIX + key, null, Object.class); + } /* @@ -304,6 +323,7 @@ public class AuthProcessDataWrapper @Override public <T> T getGenericDataFromSession(final String key, final Class<T> clazz) { return wrapStoredObject(GENERIC_PREFIX + key, null, clazz); + } /* @@ -315,7 +335,7 @@ public class AuthProcessDataWrapper @Override public void setGenericDataToSession(final String key, final Object object) throws EaafStorageException { - authProcessData.put(GENERIC_PREFIX + key, object); + authProcessData.put(GENERIC_PREFIX + key, objectSaveJsonSerialization(object)); } @@ -323,19 +343,40 @@ public class AuthProcessDataWrapper final Class<T> clazz) { if (StringUtils.isNotEmpty(key)) { final Object obj = authProcessData.get(key); - if (obj != null && clazz.isInstance(obj)) { + + // check if it is a generic container + if (obj instanceof String && ((String) obj).startsWith(GENERIC_DATA_PREFIX)) { + log.trace("Find generic JSON serialized data. Starting de-serialization ... "); + return (T) objectSaveJsonDeserialization(obj); + + // check if object is already of valid type + } else if (obj != null && clazz.isInstance(obj)) { return (T) obj; + // check if byte[] was requested and object is of type String in that case it's + // B64 content } else if (obj instanceof String && clazz.equals(byte[].class)) { - return (T) java.util.Base64.getDecoder().decode((String) obj); + try { + return (T) java.util.Base64.getDecoder().decode((String) obj); + + } catch (IllegalArgumentException e) { + log.warn("Can not decode session-object with key: {}", key, e); + + } + } else if (obj != null) { + log.warn("Can not unwrap data object with key: {} because requires class: {} but has: {}", + key, clazz.getName(), obj.getClass().getName()); } } + // return default value if we get not other result if (defaultValue == null) { return null; + } else if (clazz.isInstance(defaultValue)) { return (T) defaultValue; + } else { log.error("DefaultValue: " + defaultValue.getClass().getName() + " is not of Type:" + clazz.getName()); @@ -376,20 +417,70 @@ public class AuthProcessDataWrapper */ @Nullable public static Instant buildDateTimeUtcDate(@Nullable final String date) { - final SimpleDateFormat f = new SimpleDateFormat(PATTERN_ISSUE_INSTANT); + if (date != null) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(PATTERN_ISSUE_INSTANT); + return LocalDateTime.parse(date, dateTimeFormatter).atZone(ZoneId.of("UTC")).toInstant(); + + } + + return null; + + } + + private Object objectSaveJsonSerialization(Object object) throws EaafStorageException { try { - if (date != null) { - return f.parse(date).toInstant(); + if (object == null || object instanceof String || object instanceof Boolean + || object instanceof Integer) { + return object; - } + } else { + return GENERIC_DATA_PREFIX + DefaultJsonMapper.serialize(RawDataHolder.builder() + .object(DefaultJsonMapper.serialize(object)) + .clazzType(object.getClass().getName()) + .build()); - } catch (final ParseException e) { - log.error("Can NOT parse Date from String: {}", date, null, e); + } + } catch (EaafJsonMapperException e) { + throw new EaafStorageException("Can no serialize object to JSON", e); } + } + private Object objectSaveJsonDeserialization(Object data) { + try { + if (data == null) { + return null; + + } else if (data instanceof String) { + RawDataHolder holder = (RawDataHolder) DefaultJsonMapper.deserialize( + ((String) data).substring(GENERIC_DATA_PREFIX.length()), RawDataHolder.class); + Class<?> clz = Class.forName(holder.getClazzType()); + return DefaultJsonMapper.deserialize(holder.getObject(), clz); + + } else { + log.error("Can not deserialize: {} because it's not of type String", data.getClass().getName()); + + } + } catch (final Exception e) { + log.warn("Generic request-data object can not be casted to requested type", e); + + } return null; } + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + private static class RawDataHolder implements Serializable { + + private static final long serialVersionUID = -3827971372737399528L; + + private String object; + + private String clazzType; + + } + } diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/protocols/RequestImpl.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/protocols/RequestImpl.java index eb32d03f..a4f60ddc 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/protocols/RequestImpl.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/protocols/RequestImpl.java @@ -23,7 +23,7 @@ import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; -import java.util.Date; +import java.time.Instant; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -49,12 +49,17 @@ import at.gv.egiz.eaaf.core.api.idp.IConfigurationWithSP; import at.gv.egiz.eaaf.core.api.idp.ISpConfiguration; import at.gv.egiz.eaaf.core.exceptions.EaafAuthenticationException; import at.gv.egiz.eaaf.core.exceptions.EaafException; +import at.gv.egiz.eaaf.core.exceptions.EaafJsonMapperException; import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; import at.gv.egiz.eaaf.core.impl.http.HttpUtils; import at.gv.egiz.eaaf.core.impl.idp.auth.data.AuthProcessDataWrapper; import at.gv.egiz.eaaf.core.impl.json.EscapedJsonDeserializer; import at.gv.egiz.eaaf.core.impl.json.EscapedJsonSerializer; +import at.gv.egiz.eaaf.core.impl.utils.DefaultJsonMapper; import at.gv.egiz.eaaf.core.impl.utils.TransactionIdUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -172,9 +177,9 @@ public abstract class RequestImpl implements IRequest, Serializable { uniquePiiTransactionIdentifier = UUID.randomUUID().toString(); } - // initialize session object - genericDataStorage.put(EaafConstants.AUTH_DATA_CREATED, new Date()); + genericDataStorage.put(EaafConstants.AUTH_DATA_CREATED, + AuthProcessDataWrapper.buildDateTimeUtcString(Instant.now())); // genericDataStorage.put(EAAFConstants.VALUE_SESSIONID, // Random.nextLongRandom()); @@ -500,7 +505,7 @@ public abstract class RequestImpl implements IRequest, Serializable { @Override public final Object getRawData(final String key) { if (StringUtils.isNotEmpty(key)) { - return genericDataStorage.get(key); + return objectSaveJsonDeserialization(genericDataStorage.get(key)); } @@ -515,19 +520,18 @@ public abstract class RequestImpl implements IRequest, Serializable { if (data == null) { return null; + } try { - @SuppressWarnings("unchecked") - final T test = (T) data; - return test; + Object deserializedObject = objectSaveJsonDeserialization(data); + return deserializedObject != null ? (T) deserializedObject : null; } catch (final Exception e) { log.warn("Generic request-data object can not be casted to requested type", e); return null; } - } log.info("Can not load generic request-data with key='null'"); @@ -544,7 +548,6 @@ public abstract class RequestImpl implements IRequest, Serializable { null); } - if (object != null && !Serializable.class.isInstance(object)) { log.warn( "Generic request-data can only store objects which implements the 'Seralizable' interface"); @@ -559,7 +562,7 @@ public abstract class RequestImpl implements IRequest, Serializable { log.trace("Add generic request-data with key:" + key + " to session."); } - genericDataStorage.put(key, object); + genericDataStorage.put(key, objectSaveJsonSerialization(object)); } @@ -580,4 +583,50 @@ public abstract class RequestImpl implements IRequest, Serializable { } + private String objectSaveJsonSerialization(Object object) throws EaafStorageException { + try { + return DefaultJsonMapper.serialize(RawDataHolder.builder() + .object(DefaultJsonMapper.serialize(object)) + .clazzzType(object.getClass().getName()) + .build()); + + } catch (EaafJsonMapperException e) { + throw new EaafStorageException("Can no serialize object to JSON", e); + + } + } + + private Object objectSaveJsonDeserialization(Object data) { + try { + if (data instanceof String) { + RawDataHolder holder = (RawDataHolder) DefaultJsonMapper.deserialize( + (String) data, RawDataHolder.class); + Class<?> clz = Class.forName(holder.getClazzzType()); + return DefaultJsonMapper.deserialize(holder.getObject(), clz); + + } else { + log.error("Can not deserialize: {} because it's not of type String", data.getClass().getName()); + + } + } catch (final Exception e) { + log.warn("Generic request-data object can not be casted to requested type", e); + + } + return null; + + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + private static class RawDataHolder implements Serializable { + private static final long serialVersionUID = -8127401393932687928L; + + private String object; + + private String clazzzType; + + } + } diff --git a/eaaf_core_api/src/main/java/at/gv/egiz/eaaf/core/api/idp/auth/data/IAuthProcessDataContainer.java b/eaaf_core_api/src/main/java/at/gv/egiz/eaaf/core/api/idp/auth/data/IAuthProcessDataContainer.java index ac235a8c..afc85c7a 100644 --- a/eaaf_core_api/src/main/java/at/gv/egiz/eaaf/core/api/idp/auth/data/IAuthProcessDataContainer.java +++ b/eaaf_core_api/src/main/java/at/gv/egiz/eaaf/core/api/idp/auth/data/IAuthProcessDataContainer.java @@ -20,7 +20,6 @@ package at.gv.egiz.eaaf.core.api.idp.auth.data; import java.time.Instant; -import java.util.Date; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Stream; @@ -168,7 +167,7 @@ public interface IAuthProcessDataContainer { * * @return the sessionCreated */ - Date getSessionCreated(); + Instant getSessionCreated(); /** * Get all generic data from session. diff --git a/eaaf_core_utils/checks/spotbugs-exclude.xml b/eaaf_core_utils/checks/spotbugs-exclude.xml index 58ed1595..2b258e7c 100644 --- a/eaaf_core_utils/checks/spotbugs-exclude.xml +++ b/eaaf_core_utils/checks/spotbugs-exclude.xml @@ -30,10 +30,14 @@ </OR> </Match> <Match> - <Class name="at.gv.egiz.eaaf.core.impl.data.ExceptionContainer" /> + <OR> + <Class name="at.gv.egiz.eaaf.core.impl.data.ExceptionContainer" /> + <Class name="at.gv.egiz.eaaf.core.impl.data.ExceptionContainer$ExceptionContainerBuilder" /> + </OR> <OR> <Bug pattern="JACKSON_UNSAFE_DESERIALIZATION" /> <!-- Use custom deserialization that implements some harding --> - <Bug pattern="EI_EXPOSE_REP" /> + <Bug pattern="EI_EXPOSE_REP" /> + <Bug pattern="EI_EXPOSE_REP2" /> </OR> </Match> </FindBugsFilter>
\ No newline at end of file diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/data/ExceptionContainer.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/data/ExceptionContainer.java index cdb41147..586e819d 100644 --- a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/data/ExceptionContainer.java +++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/data/ExceptionContainer.java @@ -34,6 +34,8 @@ import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.impl.json.EscapedJsonDeserializer; import at.gv.egiz.eaaf.core.impl.json.EscapedJsonSerializer; import at.gv.egiz.eaaf.core.impl.utils.EaafSerializationUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -46,7 +48,9 @@ import lombok.Setter; */ @Getter @Setter +@Builder @NoArgsConstructor +@AllArgsConstructor public class ExceptionContainer implements Serializable { private static final long serialVersionUID = 5355860753609684995L; diff --git a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractReceiveQualEidTask.java b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractReceiveQualEidTask.java index d7af8b8e..f95d4adc 100644 --- a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractReceiveQualEidTask.java +++ b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractReceiveQualEidTask.java @@ -17,6 +17,7 @@ import at.gv.egiz.eaaf.core.api.idp.process.ExecutionContext; import at.gv.egiz.eaaf.core.exceptions.EaafAuthenticationException; import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; +import at.gv.egiz.eaaf.core.impl.data.ExceptionContainer; import at.gv.egiz.eaaf.core.impl.idp.auth.modules.AbstractAuthServletTask; import at.gv.egiz.eaaf.core.impl.utils.DataUrlBuilder; import at.gv.egiz.eaaf.core.impl.utils.StreamUtils; @@ -163,19 +164,21 @@ public abstract class AbstractReceiveQualEidTask extends AbstractAuthServletTask if (sl20Result != null) { log.debug("Received SL2.0 result: " + sl20Result); } + pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_IDENTIFIER_ERROR, - new TaskExecutionException(pendingReq, "SL2.0 Authentication FAILED. Msg: " + e.getMessage(), e)); + new ExceptionContainer(null, + new TaskExecutionException(pendingReq, "SL2.0 Authentication FAILED. Msg: " + e.getMessage(), + e))); } catch (final Exception e) { - - if (sl20Result != null) { log.debug("Received SL2.0 result: " + sl20Result); } + pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_IDENTIFIER_ERROR, - new TaskExecutionException(pendingReq, e.getMessage(), e)); + new ExceptionContainer(null, new TaskExecutionException(pendingReq, e.getMessage(), e))); } finally { // store pending request |