Clean up email subscription type

This commit is contained in:
James 2017-10-26 06:02:12 -04:00
parent 59975948b2
commit 15ba0dff03
31 changed files with 530 additions and 148 deletions

View File

@ -28,6 +28,7 @@
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<optional>true</optional>
</dependency>
<!-- Only required for narrative generator support -->

View File

@ -59,6 +59,11 @@
</dependency>
<!-- Unit test dependencies -->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@ -125,4 +130,4 @@
</plugins>
</build>
</project>
</project>

View File

@ -74,6 +74,11 @@
</dependency>
<!-- Unit test dependencies -->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>

View File

@ -32,6 +32,12 @@
<version>3.5</version>
</dependency>
-->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.search.*;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
@ -93,15 +94,6 @@ public class BaseConfig implements SchedulingConfigurer {
return new StaleSearchDeletingSvcImpl();
}
// @PostConstruct
// public void wireResourceDaos() {
// Map<String, IDao> daoBeans = myAppCtx.getBeansOfType(IDao.class);
// List bean = myAppCtx.getBean("myResourceProvidersDstu2", List.class);
// for (IDao next : daoBeans.values()) {
// next.setResourceDaos(bean);
// }
// }
@Bean
@Lazy
public SubscriptionRestHookInterceptor subscriptionRestHookInterceptor() {
@ -114,15 +106,23 @@ public class BaseConfig implements SchedulingConfigurer {
return new SubscriptionWebsocketInterceptor();
}
/**
* Note: If you're going to use this, you need to provide a bean
* of type {@link ca.uhn.fhir.jpa.subscription.email.IEmailSender}
* in your own Spring config
*/
@Bean
@Lazy
public SubscriptionEmailInterceptor subscriptionEmailInterceptor() {
return new SubscriptionEmailInterceptor();
}
@Bean
public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
retVal.setConcurrentExecutor(scheduledExecutorService().getObject());
retVal.setScheduledExecutor(scheduledExecutorService().getObject());
return retVal;
// ThreadPoolTaskScheduler retVal = new ThreadPoolTaskScheduler();
// retVal.setPoolSize(5);
// return retVal;
}
/**

View File

@ -184,7 +184,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
StopWatch w = new StopWatch();
final String searchUuid = UUID.randomUUID().toString();
ourLog.info("Registering new search {}", searchUuid);
ourLog.debug("Registering new search {}", searchUuid);
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
final ISearchBuilder sb = theCallingDao.newSearchBuilder();
@ -206,7 +206,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null) {
ourLog.info("Search {} is loading in synchronous mode", searchUuid);
ourLog.debug("Search {} is loading in synchronous mode", searchUuid);
// Execute the query and make sure we return distinct results
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
@ -561,25 +561,25 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public List<Long> getResourcePids(int theFromIndex, int theToIndex) {
ourLog.info("Requesting search PIDs from {}-{}", theFromIndex, theToIndex);
CountDownLatch latch = null;
synchronized (mySyncedPids) {
if (mySyncedPids.size() < theToIndex && mySearch.getStatus() == SearchStatusEnum.LOADING) {
int latchSize = theToIndex - mySyncedPids.size();
ourLog.trace("Registering latch to await {} results (want {} total)", latchSize, theToIndex);
latch = new CountDownLatch(latchSize);
}
}
if (latch != null) {
while (latch.getCount() > 0 && mySearch.getStatus() == SearchStatusEnum.LOADING) {
try {
ourLog.trace("Awaiting latch with {}", latch.getCount());
latch.await(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// ok
boolean keepWaiting;
do {
synchronized (mySyncedPids) {
keepWaiting = false;
if (mySyncedPids.size() < theToIndex && mySearch.getStatus() == SearchStatusEnum.LOADING) {
keepWaiting = true;
}
}
}
if (keepWaiting) {
ourLog.info("Waiting, as we only have {} results", mySyncedPids.size());
try {
Thread.sleep(500);
} catch (InterruptedException theE) {
// ignore
}
} else {
ourLog.info("Proceeding, as we have {} results", mySyncedPids.size());
}
} while (keepWaiting);
ArrayList<Long> retVal = new ArrayList<>();
synchronized (mySyncedPids) {

View File

@ -156,13 +156,11 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
try {
from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
bodyTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_BODY_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
retVal.getEmailDetails().setBodyTemplate(bodyTemplate);
}
} catch (FHIRException theE) {
@ -191,13 +189,11 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
try {
from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
bodyTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_BODY_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
retVal.getEmailDetails().setBodyTemplate(bodyTemplate);
}
List<org.hl7.fhir.r4.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");

View File

@ -131,10 +131,12 @@ public class CanonicalSubscription implements Serializable {
return myHeaders;
}
public void setHeaders(String theHeaders) {
public void setHeaders(List<? extends IPrimitiveType<String>> theHeader) {
myHeaders = new ArrayList<>();
if (isNotBlank(theHeaders)) {
myHeaders.add(theHeaders);
for (IPrimitiveType<String> next : theHeader) {
if (isNotBlank(next.getValueAsString())) {
myHeaders.add(next.getValueAsString());
}
}
}
@ -189,12 +191,10 @@ public class CanonicalSubscription implements Serializable {
}
}
public void setHeaders(List<? extends IPrimitiveType<String>> theHeader) {
public void setHeaders(String theHeaders) {
myHeaders = new ArrayList<>();
for (IPrimitiveType<String> next : theHeader) {
if (isNotBlank(next.getValueAsString())) {
myHeaders.add(next.getValueAsString());
}
if (isNotBlank(theHeaders)) {
myHeaders.add(theHeaders);
}
}
@ -212,16 +212,6 @@ public class CanonicalSubscription implements Serializable {
private String myFrom;
@JsonProperty("subjectTemplate")
private String mySubjectTemplate;
@JsonProperty("bodyTemplate")
private String myBodyTemplate;
public String getBodyTemplate() {
return myBodyTemplate;
}
public void setBodyTemplate(String theBodyTemplate) {
myBodyTemplate = theBodyTemplate;
}
public String getFrom() {
return myFrom;

View File

@ -69,15 +69,16 @@ public class SubscriptionActivatingSubscriber {
final String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode();
final String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode();
if (requestedStatus.equals(statusString)) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
status.setValueAsString(activeStatus);
ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), requestedStatus, activeStatus);
mySubscriptionDao.update(theSubscription);
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription);
}
});
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
activateSubscription(status, activeStatus, theSubscription, requestedStatus);
}
});
} else {
activateSubscription(status, activeStatus, theSubscription, requestedStatus);
}
} else if (activeStatus.equals(statusString)) {
if (!mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) {
ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
@ -91,6 +92,13 @@ public class SubscriptionActivatingSubscriber {
}
}
private void activateSubscription(IPrimitiveType<?> theStatus, String theActiveStatus, IBaseResource theSubscription, String theRequestedStatus) {
theStatus.setValueAsString(theActiveStatus);
ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus);
mySubscriptionDao.update(theSubscription);
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription);
}
public void handleMessage(RestOperationTypeEnum theOperationType, IIdType theId, final IBaseResource theSubscription) throws MessagingException {

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.subscription.email;
* #L%
*/
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.List;
public class EmailDetails {
@ -27,6 +29,7 @@ public class EmailDetails {
private String myBodyTemplate;
private List<String> myTo;
private String myFrom;
private IIdType mySubscription;
public String getBodyTemplate() {
return myBodyTemplate;
@ -52,6 +55,14 @@ public class EmailDetails {
mySubjectTemplate = theSubjectTemplate;
}
public IIdType getSubscription() {
return mySubscription;
}
public void setSubscription(IIdType theSubscription) {
mySubscription = theSubscription;
}
public List<String> getTo() {
return myTo;
}

View File

@ -22,11 +22,11 @@ package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring4.SpringTemplateEngine;
@ -35,6 +35,9 @@ import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.StringTemplateResolver;
import javax.annotation.PostConstruct;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -42,9 +45,9 @@ import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
public class EmailSender implements IEmailSender {
public class JavaMailEmailSender implements IEmailSender {
private static final Logger ourLog = LoggerFactory.getLogger(EmailSender.class);
private static final Logger ourLog = LoggerFactory.getLogger(JavaMailEmailSender.class);
private String mySmtpServerHost;
private int mySmtpServerPort = 25;
private JavaMailSenderImpl mySender;
@ -61,7 +64,8 @@ public class EmailSender implements IEmailSender {
@Override
public void send(EmailDetails theDetails) {
ourLog.info("Sending email to recipients: {}", theDetails.getTo());
String subscriptionId = theDetails.getSubscription().toUnqualifiedVersionless().getValue();
ourLog.info("Sending email for subscription {} to recipients: {}", subscriptionId, theDetails.getTo());
StopWatch sw = new StopWatch();
StringTemplateResolver templateResolver = new StringTemplateResolver();
@ -80,12 +84,18 @@ public class EmailSender implements IEmailSender {
String body = engine.process(theDetails.getBodyTemplate(), context);
String subject = engine.process(theDetails.getSubjectTemplate(), context);
SimpleMailMessage email = new SimpleMailMessage();
email.setFrom(trim(theDetails.getFrom()));
email.setTo(toTrimmedStringArray(theDetails.getTo()));
email.setSubject(subject);
email.setText(body);
email.setSentDate(new Date());
MimeMessage email = mySender.createMimeMessage();
try {
email.setFrom(trim(theDetails.getFrom()));
email.setRecipients(Message.RecipientType.TO, toTrimmedCommaSeparatedString(theDetails.getTo()));
email.setSubject(subject);
email.setText(body);
email.setSentDate(new Date());
email.addHeader("X-FHIR-Subscription", subscriptionId);
} catch (MessagingException e) {
throw new InternalErrorException("Failed to create email messaage", e);
}
mySender.send(email);
@ -106,13 +116,14 @@ public class EmailSender implements IEmailSender {
mySmtpServerPort = theSmtpServerPort;
}
private static String[] toTrimmedStringArray(List<String> theTo) {
private static String toTrimmedCommaSeparatedString(List<String> theTo) {
List<String> to = new ArrayList<>();
for (String next : theTo) {
if (isNotBlank(next)) {
to.add(next);
}
}
return to.toArray(new String[to.size()]);
return StringUtils.join(to, ",");
}
}

View File

@ -54,32 +54,29 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv
List<String> destinationAddresses = new ArrayList<>();
String[] destinationAddressStrings = StringUtils.split(endpointUrl, ",");
for (String next : destinationAddressStrings) {
next = trim(defaultString(next));
if (next.startsWith("mailto:")) {
next = next.substring("mailto:".length());
}
if (isNotBlank(next)) {
destinationAddresses.add(trim(next));
destinationAddresses.add(next);
}
}
String from = defaultString(subscription.getEmailDetails().getFrom(), provideDefaultFrom());
String from = defaultString(subscription.getEmailDetails().getFrom(), mySubscriptionEmailInterceptor.getDefaultFromAddress());
String subjectTemplate = defaultString(subscription.getEmailDetails().getSubjectTemplate(), provideDefaultSubjectTemplate());
String bodyTemplate = defaultString(subscription.getEmailDetails().getBodyTemplate(), provideDefaultBodyTemplate());
EmailDetails details = new EmailDetails();
details.setTo(destinationAddresses);
details.setFrom(from);
details.setBodyTemplate(bodyTemplate);
details.setBodyTemplate(subscription.getPayloadString());
details.setSubjectTemplate(subjectTemplate);
details.setSubscription(subscription.getIdElement(getContext()));
IEmailSender emailSender = mySubscriptionEmailInterceptor.getEmailSender();
emailSender.send(details);
}
private String provideDefaultBodyTemplate() {
return "A subscription update has been received";
}
private String provideDefaultFrom() {
return "unknown@sender.com";
}
private String provideDefaultSubjectTemplate() {
return "HAPI FHIR Subscriptions";

View File

@ -20,28 +20,51 @@ package ca.uhn.fhir.jpa.subscription.email;
* #L%
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import java.util.List;
public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
private SubscriptionDeliveringEmailSubscriber mySubscriptionDeliverySubscriber;
/**
* This is set to autowired=false just so that implementors can supply this
* with a mechanism other than autowiring if they want
*/
@Autowired(required = false)
private IEmailSender myEmailSender;
private String myDefaultFromAddress = "noreply@unknown.com";
@Override
public org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType getChannelType() {
return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.EMAIL;
}
/**
* The "from" address to use for any sent emails that to not explicitly specity a from address
*/
public String getDefaultFromAddress() {
return myDefaultFromAddress;
}
/**
* The "from" address to use for any sent emails that to not explicitly specity a from address
*/
public void setDefaultFromAddress(String theDefaultFromAddress) {
Validate.notBlank(theDefaultFromAddress, "theDefaultFromAddress must not be null or blank");
myDefaultFromAddress = theDefaultFromAddress;
}
public IEmailSender getEmailSender() {
return myEmailSender;
}
@Required
/**
* Set the email sender (this method does not need to be explicitly called if you
* are using autowiring to supply the sender)
*/
public void setEmailSender(IEmailSender theEmailSender) {
myEmailSender = theEmailSender;
}
@ -54,12 +77,12 @@ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber);
}
@PostConstruct
public void start() {
Validate.notNull(myEmailSender, "emailSender has not been configured");
super.start();
}
// @PostConstruct
// public void start() {
// Validate.notNull(myEmailSender, "emailSender has not been configured");
//
// super.start();
// }
@Override
protected void unregisterDeliverySubscriber() {

View File

@ -24,8 +24,20 @@ public class JpaConstants {
public static final String EXT_SP_UNIQUE = "http://hapifhir.io/fhir/StructureDefinition/sp-unique";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template";
public static final String EXT_SUBSCRIPTION_BODY_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-body-template";
}

View File

@ -1,27 +1,29 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
@Configuration
@EnableTransactionManagement()
@ -30,11 +32,6 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestDstu3Config.class);
private Exception myLastStackTrace;
@Bean()
public DaoConfig daoConfig() {
return new DaoConfig();
}
@Bean()
public BasicDataSource basicDataSource() {
BasicDataSource retVal = new BasicDataSource() {
@ -49,7 +46,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
ourLog.error("Exceeded maximum wait for connection", e);
logGetConnectionStackTrace();
// if ("true".equals(System.getProperty("ci"))) {
fail("Exceeded maximum wait for connection: "+ e.toString());
fail("Exceeded maximum wait for connection: " + e.toString());
// }
// System.exit(1);
retVal = null;
@ -99,20 +96,33 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
return retVal;
}
@Bean()
public DaoConfig daoConfig() {
return new DaoConfig();
}
@Bean()
@Primary()
public DataSource dataSource() {
DataSource dataSource = ProxyDataSourceBuilder
.create(basicDataSource())
.create(basicDataSource())
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS)
.countQuery()
.build();
.logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS)
.countQuery()
.build();
return dataSource;
}
@Bean
public IEmailSender emailSender() {
JavaMailEmailSender retVal = new JavaMailEmailSender();
retVal.setSmtpServerHost("localhost");
retVal.setSmtpServerPort(3025);
return retVal;
}
@Bean()
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean();

View File

@ -303,7 +303,7 @@ public abstract class BaseJpaTest {
public static void waitForSize(int theTarget, List<?> theList) {
StopWatch sw = new StopWatch();
while (theList.size() != theTarget && sw.getMillis() < 10000) {
while (theList.size() != theTarget && sw.getMillis() <= 15000) {
try {
Thread.sleep(50);
} catch (InterruptedException theE) {

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -58,6 +59,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
protected static SearchParamRegistryDstu3 ourSearchParamRegistry;
protected static DatabaseBackedPagingProvider ourPagingProvider;
protected static SubscriptionRestHookInterceptor ourRestHookSubscriptionInterceptor;
protected static SubscriptionEmailInterceptor ourEmailSubscriptionInterceptor;
protected static ISearchDao mySearchEntityDao;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
@ -111,13 +113,9 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
ourWebApplicationContext = new GenericWebApplicationContext();
ourWebApplicationContext.setParent(myAppCtx);
ourWebApplicationContext.refresh();
// ContextLoaderListener loaderListener = new ContextLoaderListener(webApplicationContext);
// loaderListener.initWebApplicationContext(mock(ServletContext.class));
//
proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext);
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// dispatcherServlet.setApplicationContext(webApplicationContext);
dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class);
ServletHolder subsServletHolder = new ServletHolder();
subsServletHolder.setServlet(dispatcherServlet);
@ -150,6 +148,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class);
mySearchEntityDao = wac.getBean(ISearchDao.class);
ourRestHookSubscriptionInterceptor = wac.getBean(SubscriptionRestHookInterceptor.class);
ourEmailSubscriptionInterceptor = wac.getBean(SubscriptionEmailInterceptor.class);
ourSearchParamRegistry = wac.getBean(SearchParamRegistryDstu3.class);
myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000);

View File

@ -163,7 +163,7 @@ public class SearchCoordinatorSvcImplTest {
params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800);
Iterator<Long> iter = new SlowIterator<Long>(pids.iterator(), 2);
Iterator<Long> iter = new SlowIterator<Long>(pids.iterator(), 1);
when(mySearchBuider.createQuery(Mockito.same(params), any(String.class))).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));

View File

@ -2,9 +2,7 @@ package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.subscription.email.EmailDetails;
import ca.uhn.fhir.jpa.subscription.email.EmailSender;
import ca.uhn.fhir.jpa.subscription.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
@ -14,9 +12,6 @@ import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import com.icegreen.greenmail.imap.ImapConstants;
import com.icegreen.greenmail.store.MailFolder;
import com.icegreen.greenmail.store.Store;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest;
@ -62,7 +57,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
public void before() throws Exception {
super.before();
EmailSender emailSender = new EmailSender();
JavaMailEmailSender emailSender = new JavaMailEmailSender();
emailSender.setSmtpServerHost("localhost");
emailSender.setSmtpServerPort(3025);
emailSender.start();
@ -129,7 +124,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
@Test
public void testSubscribeAndDeliver() throws Exception {
String payload = "application/json";
String payload = "A subscription update has been received";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
@ -153,12 +148,12 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
ourLog.info("Received: " + GreenMailUtil.getWholeMessage(messages[msgIdx]));
assertEquals("HAPI FHIR Subscriptions", messages[msgIdx].getSubject());
assertEquals(1, messages[msgIdx].getFrom().length);
assertEquals("unknown@sender.com", ((InternetAddress) messages[msgIdx].getFrom()[0]).getAddress());
assertEquals("noreply@unknown.com", ((InternetAddress) messages[msgIdx].getFrom()[0]).getAddress());
assertEquals(2, messages[msgIdx].getAllRecipients().length);
assertEquals("to1@example.com", ((InternetAddress) messages[msgIdx].getAllRecipients()[0]).getAddress());
assertEquals("to2@example.com", ((InternetAddress) messages[msgIdx].getAllRecipients()[1]).getAddress());
assertEquals(1, messages[msgIdx].getHeader("Content-Type").length);
assertEquals("text/plain; charset=UTF-8", messages[msgIdx].getHeader("Content-Type")[0]);
assertEquals("text/plain; charset=us-ascii", messages[msgIdx].getHeader("Content-Type")[0]);
String foundBody = GreenMailUtil.getBody(messages[msgIdx]);
assertEquals("A subscription update has been received", foundBody);

View File

@ -72,6 +72,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B
public void beforeReset() {
ourCreatedObservations.clear();
ourUpdatedObservations.clear();
ourRestHookSubscriptionInterceptor.initSubscriptions();
}
private void waitForQueueToDrain() throws InterruptedException {

View File

@ -0,0 +1,254 @@
package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.RestHookTestDstu2Test;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.PortUtil;
import com.google.common.collect.Lists;
import com.icegreen.greenmail.store.FolderException;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetupTest;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
/**
* Test the rest-hook subscriptions
*/
public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(EmailSubscriptionDstu3Test.class);
private static List<Observation> ourCreatedObservations = Lists.newArrayList();
private static int ourListenerPort;
private static RestfulServer ourListenerRestServer;
private static Server ourListenerServer;
private static String ourListenerServerBase;
private static List<Observation> ourUpdatedObservations = Lists.newArrayList();
private static List<String> ourContentTypes = new ArrayList<>();
private static GreenMail ourTestSmtp;
private List<IIdType> mySubscriptionIds = new ArrayList<>();
@After
public void afterUnregisterEmailListener() {
ourLog.info("**** Starting @After *****");
for (IIdType next : mySubscriptionIds){
ourClient.delete().resourceById(next).execute();
}
mySubscriptionIds.clear();
myDaoConfig.setAllowMultipleDelete(true);
ourLog.info("Deleting all subscriptions");
ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute();
ourClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute();
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(ourEmailSubscriptionInterceptor);
}
@Before
public void beforeRegisterEmailListener() throws FolderException {
ourTestSmtp.purgeEmailFromAllMailboxes();;
ourRestServer.registerInterceptor(ourEmailSubscriptionInterceptor);
ourEmailSubscriptionInterceptor.setDefaultFromAddress("123@hapifhir.io");
}
@AfterClass
public static void afterClass() {
ourTestSmtp.stop();
}
@BeforeClass
public static void beforeClass() {
ourTestSmtp = new GreenMail(ServerSetupTest.SMTP);
ourTestSmtp.start();
}
private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException {
Subscription subscription = new Subscription();
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED);
subscription.setCriteria(theCriteria);
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
channel.setType(Subscription.SubscriptionChannelType.EMAIL);
channel.setPayload(thePayload);
channel.setEndpoint("mailto:foo@example.com");
subscription.setChannel(channel);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
subscription.setId(methodOutcome.getId().getIdPart());
mySubscriptionIds.add(methodOutcome.getId());
waitForQueueToDrain();
return subscription;
}
private Observation sendObservation(String code, String system) {
Observation observation = new Observation();
CodeableConcept codeableConcept = new CodeableConcept();
observation.setCode(codeableConcept);
Coding coding = codeableConcept.addCoding();
coding.setCode(code);
coding.setSystem(system);
observation.setStatus(Observation.ObservationStatus.FINAL);
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute();
String observationId = methodOutcome.getId().getIdPart();
observation.setId(observationId);
return observation;
}
@Test
public void testEmailSubscriptionNormal() throws Exception {
String payload = "This is the body";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
createSubscription(criteria1, payload, ourListenerServerBase);
waitForQueueToDrain();
sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
List<MimeMessage> received = Arrays.asList(ourTestSmtp.getReceivedMessages());
waitForSize(1, received);
assertEquals(1, received.get(0).getFrom().length);
assertEquals("123@hapifhir.io", ((InternetAddress)received.get(0).getFrom()[0]).getAddress());
assertEquals(1, received.get(0).getAllRecipients().length);
assertEquals("foo@example.com", ((InternetAddress)received.get(0).getAllRecipients()[0]).getAddress());
assertEquals("text/plain; charset=us-ascii", received.get(0).getContentType());
assertEquals("This is the body", received.get(0).getContent().toString().trim());
assertEquals(mySubscriptionIds.get(0).toUnqualifiedVersionless().getValue(), received.get(0).getHeader("X-FHIR-Subscription")[0]);
}
@Test
public void testEmailSubscriptionWithCustom() throws Exception {
String payload = "This is the body";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
Subscription sub1 = createSubscription(criteria1, payload, ourListenerServerBase);
Subscription subscriptionTemp = ourClient.read(Subscription.class, sub1.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setValue(new StringType("myfrom@from.com"));
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setValue(new StringType("This is a subject"));
subscriptionTemp.setIdElement(subscriptionTemp.getIdElement().toUnqualifiedVersionless());
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
waitForQueueToDrain();
sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
List<MimeMessage> received = Arrays.asList(ourTestSmtp.getReceivedMessages());
waitForSize(1, received);
assertEquals(1, received.size());
assertEquals(1, received.get(0).getFrom().length);
assertEquals("myfrom@from.com", ((InternetAddress)received.get(0).getFrom()[0]).getAddress());
assertEquals(1, received.get(0).getAllRecipients().length);
assertEquals("foo@example.com", ((InternetAddress)received.get(0).getAllRecipients()[0]).getAddress());
assertEquals("text/plain; charset=us-ascii", received.get(0).getContentType());
assertEquals("This is a subject", received.get(0).getSubject().toString().trim());
assertEquals("This is the body", received.get(0).getContent().toString().trim());
assertEquals(mySubscriptionIds.get(0).toUnqualifiedVersionless().getValue(), received.get(0).getHeader("X-FHIR-Subscription")[0]);
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourEmailSubscriptionInterceptor);
}
@BeforeClass
public static void startListenerServer() throws Exception {
ourListenerPort = PortUtil.findFreePort();
ourListenerRestServer = new RestfulServer(FhirContext.forDstu3());
ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context";
ObservationListener obsListener = new ObservationListener();
ourListenerRestServer.setResourceProviders(obsListener);
ourListenerServer = new Server(ourListenerPort);
ServletContextHandler proxyHandler = new ServletContextHandler();
proxyHandler.setContextPath("/");
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(ourListenerRestServer);
proxyHandler.addServlet(servletHolder, "/fhir/context/*");
ourListenerServer.setHandler(proxyHandler);
ourListenerServer.start();
}
@AfterClass
public static void stopListenerServer() throws Exception {
ourListenerServer.stop();
}
public static class ObservationListener implements IResourceProvider {
@Create
public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
ourLog.info("Received Listener Create");
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
ourCreatedObservations.add(theObservation);
return new MethodOutcome(new IdType("Observation/1"), true);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return Observation.class;
}
@Update
public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
ourUpdatedObservations.add(theObservation);
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
ourLog.info("Received Listener Update (now have {} updates)", ourUpdatedObservations.size());
return new MethodOutcome(new IdType("Observation/1"), false);
}
}
}

View File

@ -1,8 +1,11 @@
package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetup;
import com.icegreen.greenmail.util.ServerSetupTest;
import org.hl7.fhir.dstu3.model.IdType;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
@ -15,21 +18,23 @@ import java.util.Arrays;
import static org.junit.Assert.*;
public class EmailSenderTest {
public class JavaMailEmailSenderTest {
private static final Logger ourLog = LoggerFactory.getLogger(EmailSenderTest.class);
private static final Logger ourLog = LoggerFactory.getLogger(JavaMailEmailSenderTest.class);
private static GreenMail ourTestSmtp;
private static int ourPort;
@Test
public void testSend() throws Exception {
EmailSender sender = new EmailSender();
JavaMailEmailSender sender = new JavaMailEmailSender();
sender.setSmtpServerHost("localhost");
sender.setSmtpServerPort(3025);
sender.setSmtpServerPort(ourPort);
sender.start();
String body = "foo";
EmailDetails details = new EmailDetails();
details.setSubscription(new IdType("Subscription/123"));
details.setFrom("foo@example.com ");
details.setTo(Arrays.asList(" to1@example.com", "to2@example.com "));
details.setSubjectTemplate("test subject");
@ -46,7 +51,7 @@ public class EmailSenderTest {
assertEquals("to1@example.com", ((InternetAddress)messages[0].getAllRecipients()[0]).getAddress());
assertEquals("to2@example.com", ((InternetAddress)messages[0].getAllRecipients()[1]).getAddress());
assertEquals(1, messages[0].getHeader("Content-Type").length);
assertEquals("text/plain; charset=UTF-8", messages[0].getHeader("Content-Type")[0]);
assertEquals("text/plain; charset=us-ascii", messages[0].getHeader("Content-Type")[0]);
String foundBody = GreenMailUtil.getBody(messages[0]);
assertEquals("foo", foundBody);
}
@ -58,7 +63,10 @@ public class EmailSenderTest {
@BeforeClass
public static void beforeClass() {
ourTestSmtp = new GreenMail(ServerSetupTest.SMTP);
ourPort = RandomServerPortProvider.findFreePort();
ServerSetup smtp = new ServerSetup(ourPort, null, ServerSetup.PROTOCOL_SMTP);
smtp.setServerStartupTimeout(2000);
ourTestSmtp = new GreenMail(smtp);
ourTestSmtp.start();
}

View File

@ -34,6 +34,9 @@ public class IncomingRequestAddressStrategy implements IServerAddressStrategy {
@Override
public String determineServerBase(ServletContext theServletContext, HttpServletRequest theRequest) {
if (theRequest == null) {
return null;
}
String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI());
String servletPath;

View File

@ -0,0 +1,16 @@
package ca.uhn.fhir.rest.server;
import org.junit.Test;
import static org.junit.Assert.*;
public class IncomingRequestAddressStrategyTest {
@Test
public void testRequestWithNull() {
IncomingRequestAddressStrategy s = new IncomingRequestAddressStrategy();
String result = s.determineServerBase(null, null);
assertNull(result);
}
}

View File

@ -29,6 +29,11 @@
<!--
Test dependencies on other optional parts of HAPI
-->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2.1</artifactId>

View File

@ -23,6 +23,11 @@
<!--
Test dependencies on other optional parts of HAPI
-->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2</artifactId>

View File

@ -101,6 +101,11 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
@ -122,6 +127,7 @@
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<optional>true</optional>
</dependency>
<!--

View File

@ -29,6 +29,11 @@
<!--
Test dependencies on other optional parts of HAPI
-->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2</artifactId>

View File

@ -29,6 +29,11 @@
<!--
Optional dependencies from RI codebase
-->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>es.nitaur.markdown</groupId>
<artifactId>txtmark</artifactId>

View File

@ -100,6 +100,11 @@
</dependency>
<!-- Test Deps -->
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-converter</artifactId>

View File

@ -17,7 +17,6 @@
</ul>
]]>
</action>
<action type="fix">
The Android client module has been restored to working order, and no longer
requires a special classifier or an XML parser to be present in order to