GData html render preview: LUCENE-660

git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@433051 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yonik Seeley 2006-08-20 21:20:08 +00:00
parent 9763d03f48
commit 8c0d242750
12 changed files with 238 additions and 55 deletions

View File

@ -58,6 +58,7 @@ public class GDataRequest {
private static final String RESPONSE_FORMAT_PARAMETER = "alt"; private static final String RESPONSE_FORMAT_PARAMETER = "alt";
private static final String RESPONSE_FORMAT_PARAMETER_RSS = "rss"; private static final String RESPONSE_FORMAT_PARAMETER_RSS = "rss";
private static final String RESPONSE_FORMAT_PARAMETER_HTML = "html";
private static final int DEFAULT_ITEMS_PER_PAGE = 25; private static final int DEFAULT_ITEMS_PER_PAGE = 25;
@ -276,6 +277,8 @@ public class GDataRequest {
return; return;
if (formatParameter.equalsIgnoreCase(RESPONSE_FORMAT_PARAMETER_RSS)) if (formatParameter.equalsIgnoreCase(RESPONSE_FORMAT_PARAMETER_RSS))
this.responseFormat = OutputFormat.RSS; this.responseFormat = OutputFormat.RSS;
if (formatParameter.equalsIgnoreCase(RESPONSE_FORMAT_PARAMETER_HTML))
this.responseFormat = OutputFormat.HTML;
} }
@ -439,7 +442,11 @@ public class GDataRequest {
/** /**
* Output format RSS * Output format RSS
*/ */
RSS RSS,
/**
* Output format html if user defined xsl style sheet is present
*/
HTML
} }
/** /**

View File

@ -17,14 +17,23 @@
package org.apache.lucene.gdata.server; package org.apache.lucene.gdata.server;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
import java.util.Date; import java.util.Date;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.lucene.gdata.server.GDataRequest.OutputFormat; import org.apache.lucene.gdata.server.GDataRequest.OutputFormat;
import org.apache.lucene.gdata.server.registry.ProvidedService;
import org.apache.lucene.gdata.utils.DateFormater; import org.apache.lucene.gdata.utils.DateFormater;
import com.google.gdata.data.BaseEntry; import com.google.gdata.data.BaseEntry;
@ -104,7 +113,7 @@ public class GDataResponse {
public static final int UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED; public static final int UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED;
private static final Log LOG = LogFactory.getLog(GDataResponse.class); static final Log LOG = LogFactory.getLog(GDataResponse.class);
private int error; private int error;
private boolean isError = false; private boolean isError = false;
@ -119,11 +128,6 @@ public class GDataResponse {
protected static final String XMLMIME_RSS = "text/xml"; protected static final String XMLMIME_RSS = "text/xml";
private static final String DEFAUL_NAMESPACE_URI = "http://www.w3.org/2005/Atom";
private static final Namespace DEFAULT_NAMESPACE = new Namespace("",
DEFAUL_NAMESPACE_URI);
private static final String HEADER_LASTMODIFIED = "Last-Modified"; private static final String HEADER_LASTMODIFIED = "Last-Modified";
/** /**
@ -188,33 +192,26 @@ public class GDataResponse {
* *
* @param feed - * @param feed -
* the feed to respond to the client * the feed to respond to the client
* @param profile - * @param service - the service to render the feed
* the extension profile for the feed to write *
* @throws IOException - * @throws IOException -
* if an I/O exception occurs, often caused by an already * if an I/O exception occurs, often caused by an already
* closed Writer or OutputStream * closed Writer or OutputStream
* *
*/ */
public void sendResponse(BaseFeed feed, ExtensionProfile profile) public void sendResponse(final BaseFeed feed, final ProvidedService service)
throws IOException { throws IOException {
if (feed == null) if (feed == null)
throw new IllegalArgumentException("feed must not be null"); throw new IllegalArgumentException("feed must not be null");
if (profile == null) if (service == null)
throw new IllegalArgumentException( throw new IllegalArgumentException(
"extension profile must not be null"); "provided service must not be null");
DateTime time = feed.getUpdated(); DateTime time = feed.getUpdated();
if (time != null) if (time != null)
setLastModifiedHeader(time.getValue()); setLastModifiedHeader(time.getValue());
XmlWriter writer = createWriter(); FormatWriter writer = FormatWriter.getFormatWriter(this,service);
writer.generateOutputFormat(feed,this.response);
if (this.outputFormat.equals(OutputFormat.ATOM)) {
this.response.setContentType(XMLMIME_ATOM);
feed.generateAtom(writer, profile);
} else {
this.response.setContentType(XMLMIME_RSS);
feed.generateRss(writer, profile);
}
writer.close();
} }
/** /**
@ -226,38 +223,27 @@ public class GDataResponse {
* *
* @param entry - * @param entry -
* the modified / created entry to send * the modified / created entry to send
* @param profile - * @param service - the service to render the feed
* the entries extension profile
* @throws IOException - * @throws IOException -
* if an I/O exception occurs, often caused by an already * if an I/O exception occurs, often caused by an already
* closed Writer or OutputStream * closed Writer or OutputStream
*/ */
public void sendResponse(BaseEntry entry, ExtensionProfile profile) public void sendResponse(BaseEntry entry, ProvidedService service)
throws IOException { throws IOException {
if (entry == null) if (entry == null)
throw new IllegalArgumentException("entry must not be null"); throw new IllegalArgumentException("entry must not be null");
if (profile == null) if (service == null)
throw new IllegalArgumentException( throw new IllegalArgumentException(
"extension profile must not be null"); "service must not be null");
DateTime time = entry.getUpdated(); DateTime time = entry.getUpdated();
if (time != null) if (time != null)
setLastModifiedHeader(time.getValue()); setLastModifiedHeader(time.getValue());
XmlWriter writer = createWriter(); FormatWriter writer = FormatWriter.getFormatWriter(this,service);
if (this.outputFormat.equals(OutputFormat.ATOM)) writer.generateOutputFormat(entry,this.response);
entry.generateAtom(writer, profile);
else
entry.generateRss(writer, profile);
writer.close();
} }
private XmlWriter createWriter() throws IOException {
XmlWriter writer = new XmlWriter(getWriter(), this.encoding);
// set the default namespace to Atom if Atom is the response format
if (this.outputFormat.equals(OutputFormat.ATOM))
writer.setDefaultNamespace(DEFAULT_NAMESPACE);
return writer;
}
/** /**
* This encoding will be used to encode the xml representation of feed or * This encoding will be used to encode the xml representation of feed or
@ -326,4 +312,138 @@ public class GDataResponse {
this.response.setStatus(status); this.response.setStatus(status);
} }
private static abstract class FormatWriter{
static FormatWriter getFormatWriter(final GDataResponse response, final ProvidedService service ){
OutputFormat format = response.getOutputFormat();
if(format == OutputFormat.HTML){
return new HTMLFormatWriter(service);
}
return new SyndicateFormatWriter(service,format,response.getEncoding());
}
abstract void generateOutputFormat(final BaseFeed feed, final HttpServletResponse response) throws IOException;
abstract void generateOutputFormat(final BaseEntry entry, final HttpServletResponse response) throws IOException;
private static class HTMLFormatWriter extends FormatWriter{
private static final String CONTENT_TYPE = "text/html";
private final ProvidedService service;
HTMLFormatWriter(final ProvidedService service){
this.service = service;
}
@Override
void generateOutputFormat(BaseFeed feed, final HttpServletResponse response) throws IOException {
Templates template = this.service.getTransformTemplate();
response.setContentType(CONTENT_TYPE);
if(template == null){
sendNotAvailable(response);
return;
}
StringWriter writer = new StringWriter();
XmlWriter xmlWriter = new XmlWriter(writer);
feed.generateAtom(xmlWriter,this.service.getExtensionProfile());
try {
writeHtml(template,response.getWriter(),writer);
} catch (TransformerException e) {
LOG.error("Can not transform feed for service "+this.service.getName(),e);
sendNotAvailable(response);
}
}
@Override
void generateOutputFormat(BaseEntry entry, final HttpServletResponse response) throws IOException{
Templates template = this.service.getTransformTemplate();
response.setContentType(CONTENT_TYPE);
if(template == null){
sendNotAvailable(response);
return;
}
StringWriter writer = new StringWriter();
XmlWriter xmlWriter = new XmlWriter(writer);
entry.generateAtom(xmlWriter,this.service.getExtensionProfile());
try {
writeHtml(template,response.getWriter(),writer);
} catch (TransformerException e) {
LOG.error("Can not transform feed for service "+this.service.getName(),e);
sendNotAvailable(response);
}
}
private void writeHtml(final Templates template, final Writer writer, final StringWriter source ) throws TransformerException{
Transformer transformer = template.newTransformer();
Source tranformSource = new StreamSource(new StringReader(source.toString()));
transformer.transform(tranformSource,new StreamResult(writer));
}
private void sendNotAvailable(final HttpServletResponse response) throws IOException{
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "No transformation stylesheet available");
}
}
private static class SyndicateFormatWriter extends FormatWriter{
private static final String DEFAUL_NAMESPACE_URI = "http://www.w3.org/2005/Atom";
private static final Namespace DEFAULT_NAMESPACE = new Namespace("",
DEFAUL_NAMESPACE_URI);
private final ProvidedService service;
private final String encoding;
private final OutputFormat format;
SyndicateFormatWriter(final ProvidedService service,final OutputFormat format, String encoding){
this.service = service;
this.format = format;
this.encoding = encoding;
}
@Override
void generateOutputFormat(final BaseFeed feed, final HttpServletResponse response) throws IOException {
XmlWriter writer = null;
try{
writer = createWriter(response.getWriter());
if (this.format == OutputFormat.ATOM) {
response.setContentType(XMLMIME_ATOM);
feed.generateAtom(writer, this.service.getExtensionProfile());
} else {
response.setContentType(XMLMIME_RSS);
feed.generateRss(writer, this.service.getExtensionProfile());
}
}finally{
if(writer != null)
writer.close();
}
}
@Override
void generateOutputFormat(final BaseEntry entry, final HttpServletResponse response) throws IOException {
XmlWriter writer = null;
try{
writer = createWriter(response.getWriter());
if (this.format == OutputFormat.ATOM) {
response.setContentType(XMLMIME_ATOM);
entry.generateAtom(writer, this.service.getExtensionProfile());
} else {
response.setContentType(XMLMIME_RSS);
entry.generateRss(writer, this.service.getExtensionProfile());
}
}finally{
if(writer != null)
writer.close();
}
}
private XmlWriter createWriter(final Writer target) throws IOException {
XmlWriter writer = new XmlWriter(target, this.encoding);
// set the default namespace to Atom if Atom is the response format
if (this.format == OutputFormat.ATOM)
writer.setDefaultNamespace(DEFAULT_NAMESPACE);
return writer;
}
}
}
} }

View File

@ -15,6 +15,8 @@
*/ */
package org.apache.lucene.gdata.server.registry; package org.apache.lucene.gdata.server.registry;
import javax.xml.transform.Templates;
import org.apache.lucene.gdata.search.config.IndexSchema; import org.apache.lucene.gdata.search.config.IndexSchema;
import com.google.gdata.data.ExtensionProfile; import com.google.gdata.data.ExtensionProfile;
@ -55,4 +57,9 @@ public interface ProvidedService {
* @return the index schema configuration for this service * @return the index schema configuration for this service
*/ */
public abstract IndexSchema getIndexSchema(); public abstract IndexSchema getIndexSchema();
/**
* @return the compiled xslt stylesheet to transform the feed / entry for preview
*/
public abstract Templates getTransformTemplate();
} }

View File

@ -17,6 +17,11 @@ package org.apache.lucene.gdata.server.registry;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.lucene.gdata.search.config.IndexSchema; import org.apache.lucene.gdata.search.config.IndexSchema;
@ -26,9 +31,7 @@ import org.apache.lucene.gdata.utils.SimpleObjectPool;
import com.google.gdata.data.BaseEntry; import com.google.gdata.data.BaseEntry;
import com.google.gdata.data.BaseFeed; import com.google.gdata.data.BaseFeed;
import com.google.gdata.data.Entry;
import com.google.gdata.data.ExtensionProfile; import com.google.gdata.data.ExtensionProfile;
import com.google.gdata.data.Feed;
/** /**
* Standard implementation of * Standard implementation of
@ -78,6 +81,9 @@ public class ProvidedServiceConfig implements ProvidedService, ScopeVisitor {
private ExtensionProfile extensionProfile; private ExtensionProfile extensionProfile;
private int poolSize = DEFAULT_POOL_SIZE; private int poolSize = DEFAULT_POOL_SIZE;
private Templates transformerTemplate;
/** /**
* @return Returns the poolSize. * @return Returns the poolSize.
@ -326,4 +332,33 @@ public class ProvidedServiceConfig implements ProvidedService, ScopeVisitor {
this.indexSchema.setName(this.serviceName); this.indexSchema.setName(this.serviceName);
} }
/**
* @see org.apache.lucene.gdata.server.registry.ProvidedService#getTransformTemplate()
*/
public Templates getTransformTemplate() {
return this.transformerTemplate;
}
/**
* Sets and creates the preview transformer xslt template to provide a html formate for feeds and entries.
* The given file name must be available in the classpath.
* @param filename - the name of the file in the classpath
*/
public void setXsltStylesheet(String filename){
if(filename == null || filename.length() == 0){
LOG.info("No preview stylesheet configured for service "+this.serviceName);
return;
}
TransformerFactory factory = TransformerFactory.newInstance();
try {
this.transformerTemplate = factory.newTemplates(new StreamSource(ProvidedServiceConfig.class.getResourceAsStream(filename.startsWith("/")?filename:"/"+filename)));
} catch (TransformerConfigurationException e) {
throw new RuntimeException("Can not compile xslt stylesheet path: "+filename,e);
}
}
} }

View File

@ -77,6 +77,7 @@ class RegistryBuilder {
digester.addBeanPropertySetter("gdata/service/entry-class", "entryType"); digester.addBeanPropertySetter("gdata/service/entry-class", "entryType");
digester.addBeanPropertySetter("gdata/service/extension-profile", digester.addBeanPropertySetter("gdata/service/extension-profile",
"extensionProfileClass"); "extensionProfileClass");
digester.addBeanPropertySetter("gdata/service/previewStyleSheet","xsltStylesheet");
addIndexRule(digester); addIndexRule(digester);
/* /*
* load components and configurations * load components and configurations

View File

@ -90,12 +90,12 @@ public class DefaultGetHandler extends AbstractGdataRequestHandler {
this.feedResponse); this.feedResponse);
this.feedResponse.sendResponse(feed, this.feedRequest this.feedResponse.sendResponse(feed, this.feedRequest
.getConfigurator().getExtensionProfile()); .getConfigurator());
} else { } else {
BaseEntry entry = this.service.getSingleEntry(this.feedRequest, BaseEntry entry = this.service.getSingleEntry(this.feedRequest,
this.feedResponse); this.feedResponse);
this.feedResponse.sendResponse(entry, this.feedRequest this.feedResponse.sendResponse(entry, this.feedRequest
.getConfigurator().getExtensionProfile()); .getConfigurator());
} }
} catch (ServiceException e) { } catch (ServiceException e) {

View File

@ -74,7 +74,7 @@ public class DefaultInsertHandler extends AbstractGdataRequestHandler {
BaseEntry entry = this.service.createEntry(this.feedRequest,this.feedResponse); BaseEntry entry = this.service.createEntry(this.feedRequest,this.feedResponse);
setFeedResponseFormat(); setFeedResponseFormat();
setFeedResponseStatus(GDataResponse.CREATED); setFeedResponseStatus(GDataResponse.CREATED);
this.feedResponse.sendResponse(entry, this.feedRequest.getConfigurator().getExtensionProfile()); this.feedResponse.sendResponse(entry, this.feedRequest.getConfigurator());
}catch (ServiceException e) { }catch (ServiceException e) {
LOG.error("Could not process GetFeed request - "+e.getMessage(),e); LOG.error("Could not process GetFeed request - "+e.getMessage(),e);

View File

@ -75,7 +75,7 @@ public class DefaultUpdateHandler extends AbstractGdataRequestHandler {
BaseEntry entry = this.service.updateEntry(this.feedRequest, BaseEntry entry = this.service.updateEntry(this.feedRequest,
this.feedResponse); this.feedResponse);
setFeedResponseFormat(); setFeedResponseFormat();
this.feedResponse.sendResponse(entry, this.feedRequest.getConfigurator().getExtensionProfile()); this.feedResponse.sendResponse(entry, this.feedRequest.getConfigurator());
} }
catch (ServiceException e) { catch (ServiceException e) {

View File

@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.apache.lucene.gdata.server.GDataRequest.OutputFormat; import org.apache.lucene.gdata.server.GDataRequest.OutputFormat;
import org.apache.lucene.gdata.utils.ProvidedServiceStub;
import org.easymock.MockControl; import org.easymock.MockControl;
import com.google.gdata.data.Entry; import com.google.gdata.data.Entry;
@ -69,7 +70,7 @@ public class TestGDataResponse extends TestCase {
public void testSendResponseBaseFeedExtensionProfile() throws IOException { public void testSendResponseBaseFeedExtensionProfile() throws IOException {
try{ try{
Feed f = null; Feed f = null;
this.response.sendResponse(f,new ExtensionProfile()); this.response.sendResponse(f, new ProvidedServiceStub());
fail("Exception expected"); fail("Exception expected");
}catch (IllegalArgumentException e) { }catch (IllegalArgumentException e) {
// //
@ -90,7 +91,7 @@ public class TestGDataResponse extends TestCase {
this.response.setOutputFormat(OutputFormat.ATOM); this.response.setOutputFormat(OutputFormat.ATOM);
this.control.replay(); this.control.replay();
this.response.sendResponse(createFeed(),new ExtensionProfile()); this.response.sendResponse(createFeed(), new ProvidedServiceStub());
assertEquals("Simple XML representation",stringWriter.toString(),generatedFeedAtom); assertEquals("Simple XML representation",stringWriter.toString(),generatedFeedAtom);
this.control.reset(); this.control.reset();
@ -102,8 +103,7 @@ public class TestGDataResponse extends TestCase {
this.httpResponse.setContentType(GDataResponse.XMLMIME_RSS); this.httpResponse.setContentType(GDataResponse.XMLMIME_RSS);
this.control.replay(); this.control.replay();
this.response.sendResponse(createFeed(),new ExtensionProfile this.response.sendResponse(createFeed(), new ProvidedServiceStub());
());
assertEquals("Simple XML representation",stringWriter.toString(),generatedFeedRSS); assertEquals("Simple XML representation",stringWriter.toString(),generatedFeedRSS);
@ -117,7 +117,7 @@ public class TestGDataResponse extends TestCase {
public void testSendResponseBaseEntryExtensionProfile() throws IOException { public void testSendResponseBaseEntryExtensionProfile() throws IOException {
try{ try{
Entry e = null; Entry e = null;
this.response.sendResponse(e,new ExtensionProfile()); this.response.sendResponse(e, new ProvidedServiceStub());
fail("Exception expected"); fail("Exception expected");
}catch (IllegalArgumentException e) { }catch (IllegalArgumentException e) {
// //
@ -134,11 +134,11 @@ public class TestGDataResponse extends TestCase {
PrintWriter writer = new PrintWriter(stringWriter); PrintWriter writer = new PrintWriter(stringWriter);
this.control.expectAndReturn(this.httpResponse.getWriter(),writer); this.control.expectAndReturn(this.httpResponse.getWriter(),writer);
this.httpResponse.setContentType(GDataResponse.XMLMIME_ATOM);
this.response.setOutputFormat(OutputFormat.ATOM); this.response.setOutputFormat(OutputFormat.ATOM);
this.control.replay(); this.control.replay();
this.response.sendResponse(createEntry(),new ExtensionProfile this.response.sendResponse(createEntry(), new ProvidedServiceStub());
());
assertEquals("Simple XML representation ATOM",stringWriter.toString(),generatedEntryAtom); assertEquals("Simple XML representation ATOM",stringWriter.toString(),generatedEntryAtom);
// test rss output // test rss output
@ -147,11 +147,11 @@ public class TestGDataResponse extends TestCase {
writer = new PrintWriter(stringWriter); writer = new PrintWriter(stringWriter);
this.control.expectAndReturn(this.httpResponse.getWriter(),writer); this.control.expectAndReturn(this.httpResponse.getWriter(),writer);
this.httpResponse.setContentType(GDataResponse.XMLMIME_RSS);
this.response.setOutputFormat(OutputFormat.RSS); this.response.setOutputFormat(OutputFormat.RSS);
this.control.replay(); this.control.replay();
this.response.sendResponse(createEntry(),new ExtensionProfile this.response.sendResponse(createEntry(), new ProvidedServiceStub());
());
assertEquals("Simple XML representation RSS",stringWriter.toString(),generatedEntryRSS); assertEquals("Simple XML representation RSS",stringWriter.toString(),generatedEntryRSS);

View File

@ -15,6 +15,8 @@
*/ */
package org.apache.lucene.gdata.utils; package org.apache.lucene.gdata.utils;
import javax.xml.transform.Templates;
import org.apache.lucene.gdata.search.config.IndexSchema; import org.apache.lucene.gdata.search.config.IndexSchema;
import org.apache.lucene.gdata.server.registry.ProvidedService; import org.apache.lucene.gdata.server.registry.ProvidedService;
@ -63,4 +65,11 @@ public class ProvidedServiceStub implements ProvidedService {
return this.indexSchema; return this.indexSchema;
} }
public Templates getTransformTemplate() {
return null;
}
} }

View File

@ -6,6 +6,7 @@
<extension-profile> <extension-profile>
com.google.gdata.data.ExtensionProfile com.google.gdata.data.ExtensionProfile
</extension-profile> </extension-profile>
<previewStyleSheet>transform.xslt</previewStyleSheet>
<index-schema defaultSearchField="content"> <index-schema defaultSearchField="content">
<index useTimedIndexer="true" indexerIdleTime="120" optimizeAfterCommit="5" commitAfterDocuments="10"> <index useTimedIndexer="true" indexerIdleTime="120" optimizeAfterCommit="5" commitAfterDocuments="10">
<defaultAnalyzer> <defaultAnalyzer>

View File

@ -3,6 +3,7 @@
<xs:element name="feed-class" type="xs:string" /> <xs:element name="feed-class" type="xs:string" />
<xs:element name="entry-class" type="xs:string" /> <xs:element name="entry-class" type="xs:string" />
<xs:element name="extension-profile" type="xs:string" /> <xs:element name="extension-profile" type="xs:string" />
<xs:element name="previewStyleSheet" type="xs:string" />
<xs:element name="class" type="xs:string" /> <xs:element name="class" type="xs:string" />
<xs:annotation> <xs:annotation>
<xs:documentation xml:lang="en"> <xs:documentation xml:lang="en">
@ -27,6 +28,8 @@
minOccurs="1" /> minOccurs="1" />
<xs:element ref="extension-profile" maxOccurs="1" <xs:element ref="extension-profile" maxOccurs="1"
minOccurs="1" /> minOccurs="1" />
<xs:element ref="previewStyleSheet" maxOccurs="1"
minOccurs="0" />
<xs:element ref="index-schema" maxOccurs="1" <xs:element ref="index-schema" maxOccurs="1"
minOccurs="1" /> minOccurs="1" />
</xs:sequence> </xs:sequence>