VIVO-731 Replace SparqlQueryServlet with SparqlQueryController

SparqlQueryServlet was JSP-based, so delete the JSP also.
SparqlQueryController is Freemarker-based, and is a this shell around the SparqlQueryApiExecutor.
This commit is contained in:
Jim Blake 2014-04-14 12:19:48 -04:00
parent 0c0915ef65
commit de32d53791
7 changed files with 264 additions and 1560 deletions

View file

@ -1,476 +0,0 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.github.jsonldjava.core.JSONLD;
import com.github.jsonldjava.core.JSONLDProcessingError;
import com.github.jsonldjava.impl.JenaRDFParser;
import com.github.jsonldjava.utils.JSONUtils;
import com.hp.hpl.jena.query.Query;
import com.hp.hpl.jena.query.QuerySolution;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.query.ResultSetFactory;
import com.hp.hpl.jena.query.ResultSetFormatter;
import com.hp.hpl.jena.rdf.model.Literal;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.sparql.resultset.ResultSetFormat;
import com.hp.hpl.jena.vocabulary.XSD;
import edu.cornell.mannlib.vedit.controller.BaseEditController;
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
import edu.cornell.mannlib.vitro.webapp.beans.Ontology;
import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ModelSerializationFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ResultFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
import edu.cornell.mannlib.vitro.webapp.utils.SparqlQueryUtils;
import edu.cornell.mannlib.vitro.webapp.web.ContentType;
/**
* Services a SPARQL query. This will return a simple error message and a 501 if
* there is no Model.
*
*
* @author bdc34
*
*/
public class SparqlQueryServlet extends BaseEditController {
private static final Log log = LogFactory.getLog(SparqlQueryServlet.class.getName());
/**
* format configurations for SELECT queries.
*/
protected static HashMap<String,RSFormatConfig> rsFormats = new HashMap<String,RSFormatConfig>();
/**
* format configurations for CONSTRUCT/DESCRIBE queries.
*/
protected static HashMap<String,ModelFormatConfig> modelFormats =
new HashMap<String,ModelFormatConfig>();
/**
* Use this map to decide which MIME type is suited for the "accept" header.
*/
public static final Map<String, Float> ACCEPTED_CONTENT_TYPES;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
this.doGet(request,response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
VitroRequest vreq = new VitroRequest(request);
//first check if the email and password are just in the request
String email = vreq.getParameter("email");
String password = vreq.getParameter("password");
boolean isAuth = PolicyHelper.isAuthorizedForActions(vreq,
email, password, SimplePermission.USE_SPARQL_QUERY_PAGE.ACTIONS);
//otherwise use the normal auth mechanism
if( ! isAuth &&
!isAuthorizedToDisplayPage(request, response,
SimplePermission.USE_SPARQL_QUERY_PAGE.ACTIONS)) {
return;
}
Model model = vreq.getJenaOntModel();
if( model == null ){
doNoModelInContext(response);
return;
}
// Use RDFService from context to avoid language filtering
RDFService rdfService = RDFServiceUtils.getRDFServiceFactory(
getServletContext()).getRDFService();
String queryParam = vreq.getParameter("query");
log.debug("queryParam was : " + queryParam);
if( queryParam == null || "".equals(queryParam) ){
doHelp(request,response);
return;
}
String contentType = checkForContentType(vreq.getHeader("Accept"));
Query query = SparqlQueryUtils.create(queryParam);
if( query.isSelectType() ){
String format = contentType!=null ? contentType:vreq.getParameter("resultFormat");
RSFormatConfig formatConf = rsFormats.get(format);
doSelect(response, queryParam, formatConf, rdfService);
}else if( query.isAskType()){
doAsk( queryParam, rdfService, response );
}else if( query.isConstructType() || query.isDescribeType() ){
String format = contentType != null ? contentType : vreq.getParameter("rdfResultFormat");
if (format== null) {
format= "RDF/XML-ABBREV";
}
ModelFormatConfig formatConf = modelFormats.get(format);
doConstruct(response, query, formatConf, rdfService);
}else{
doHelp(request,response);
}
return;
}
private void doAsk(String queryParam, RDFService rdfService,
HttpServletResponse response) throws ServletException, IOException {
// Irrespective of the ResultFormatParam,
// this always prints a boolean to the default OutputStream.
String result;
try {
result = (rdfService.sparqlAskQuery(queryParam) == true)
? "true"
: "false";
} catch (RDFServiceException e) {
throw new ServletException( "Could not execute ask query ", e );
}
PrintWriter p = response.getWriter();
p.write(result);
return;
}
/**
* Execute the query and send the result to out. Attempt to
* send the RDFService the same format as the rdfResultFormatParam
* so that the results from the RDFService can be directly piped to the client.
*/
private void doSelect(HttpServletResponse response,
String queryParam,
RSFormatConfig formatConf,
RDFService rdfService
) throws ServletException {
try {
if( ! formatConf.converstionFromWireFormat ){
response.setContentType( formatConf.responseMimeType );
InputStream results;
results = rdfService.sparqlSelectQuery(queryParam, formatConf.wireFormat );
pipe( results, response.getOutputStream() );
}else{
//always use JSON when conversion is needed.
InputStream results = rdfService.sparqlSelectQuery(queryParam, ResultFormat.JSON );
response.setContentType( formatConf.responseMimeType );
ResultSet rs = ResultSetFactory.fromJSON( results );
OutputStream out = response.getOutputStream();
ResultSetFormatter.output(out, rs, formatConf.jenaResponseFormat);
}
} catch (RDFServiceException e) {
throw new ServletException("Cannot get result from the RDFService",e);
} catch (IOException e) {
throw new ServletException("Cannot perform SPARQL SELECT",e);
}
}
/**
* Execute the query and send the result to out. Attempt to
* send the RDFService the same format as the rdfResultFormatParam
* so that the results from the RDFService can be directly piped to the client.
* @param rdfService
* @throws IOException
* @throws RDFServiceException
* @throws
*/
private void doConstruct( HttpServletResponse response,
Query query,
ModelFormatConfig formatConfig,
RDFService rdfService
) throws ServletException{
try{
InputStream rawResult = null;
if( query.isConstructType() ){
rawResult= rdfService.sparqlConstructQuery( query.toString(), formatConfig.wireFormat );
}else if ( query.isDescribeType() ){
rawResult = rdfService.sparqlDescribeQuery( query.toString(), formatConfig.wireFormat );
}
response.setContentType( formatConfig.responseMimeType );
if( formatConfig.converstionFromWireFormat ){
Model resultModel = RDFServiceUtils.parseModel( rawResult, formatConfig.wireFormat );
if( "JSON-LD".equals( formatConfig.jenaResponseFormat )){
//since jena 2.6.4 doesn't support JSON-LD we do it
try {
JenaRDFParser parser = new JenaRDFParser();
Object json = JSONLD.fromRDF(resultModel, parser);
JSONUtils.write(response.getWriter(), json);
} catch (JSONLDProcessingError e) {
throw new RDFServiceException("Could not convert from Jena model to JSON-LD", e);
}
}else{
OutputStream out = response.getOutputStream();
resultModel.write(out, formatConfig.jenaResponseFormat );
}
}else{
OutputStream out = response.getOutputStream();
pipe( rawResult, out );
}
}catch( IOException ex){
throw new ServletException("could not run SPARQL CONSTRUCT",ex);
} catch (RDFServiceException ex) {
throw new ServletException("could not run SPARQL CONSTRUCT",ex);
}
}
private void pipe( InputStream in, OutputStream out) throws IOException{
int size;
byte[] buffer = new byte[4096];
while( (size = in.read(buffer)) > -1 ) {
out.write(buffer,0,size);
}
}
private void doNoModelInContext(HttpServletResponse res){
try {
res.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
ServletOutputStream sos = res.getOutputStream();
sos.println("<html><body>this service is not supporeted by the current " +
"webapp configuration. A jena model is required in the servlet context.</body></html>" );
} catch (IOException e) {
log.error("Could not write to ServletOutputStream");
}
}
private void toCsv(Writer out, ResultSet results) {
// The Skife library wouldn't quote and escape the normal way,
// so I'm trying it manually.
List<String> varNames = results.getResultVars();
int width = varNames.size();
while (results.hasNext()) {
QuerySolution solution = (QuerySolution) results.next();
String[] valueArray = new String[width];
Iterator<String> varNameIt = varNames.iterator();
int index = 0;
while (varNameIt.hasNext()) {
String varName = varNameIt.next();
String value = null;
try {
Literal lit = solution.getLiteral(varName);
if (lit != null) {
value = lit.getLexicalForm();
if (XSD.anyURI.getURI().equals(lit.getDatatypeURI())) {
value = URLDecoder.decode(value, "UTF-8");
}
}
} catch (Exception e) {
try {
Resource res = solution.getResource(varName);
if (res != null) {
if (res.isAnon()) {
value = res.getId().toString();
} else {
value = res.getURI();
}
}
} catch (Exception f) {}
}
valueArray[index] = value;
index++;
}
StringBuffer rowBuff = new StringBuffer();
for (int i = 0; i < valueArray.length; i++) {
String value = valueArray[i];
if (value != null) {
value.replaceAll("\"", "\\\"");
rowBuff.append("\"").append(value).append("\"");
}
if (i + 1 < width) {
rowBuff.append(",");
}
}
rowBuff.append("\n");
try {
out.write(rowBuff.toString());
} catch (IOException ioe) {
log.error(ioe);
}
}
try {
out.flush();
} catch (IOException ioe) {
log.error(ioe);
}
}
private void doHelp(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
VitroRequest vreq = new VitroRequest(req);
OntologyDao daoObj = vreq.getUnfilteredWebappDaoFactory().getOntologyDao();
List<Ontology> ontologiesObj = daoObj.getAllOntologies();
ArrayList<String> prefixList = new ArrayList<String>();
if(ontologiesObj !=null && ontologiesObj.size()>0){
for(Ontology ont: ontologiesObj) {
prefixList.add(ont.getPrefix() == null ? "(not yet specified)" : ont.getPrefix());
prefixList.add(ont.getURI() == null ? "" : ont.getURI());
}
}
else{
prefixList.add("<strong>" + "No Ontologies added" + "</strong>");
prefixList.add("<strong>" + "Load Ontologies" + "</strong>");
}
req.setAttribute("prefixList", prefixList);
req.setAttribute("title","SPARQL Query");
req.setAttribute("bodyJsp", "/admin/sparqlquery/sparqlForm.jsp");
RequestDispatcher rd = req.getRequestDispatcher("/"+Controllers.BASIC_JSP);
rd.forward(req,res);
}
/** Simple boolean vaule to improve the legibility of confiugrations. */
private final static boolean CONVERT = true;
/** Simple vaule to improve the legibility of confiugrations. */
private final static String NO_CONVERSION = null;
public static class FormatConfig{
public String valueFromForm;
public boolean converstionFromWireFormat;
public String responseMimeType;
}
private static ModelFormatConfig[] fmts = {
new ModelFormatConfig("RDF/XML",
!CONVERT, ModelSerializationFormat.RDFXML, NO_CONVERSION, "application/rdf+xml" ),
new ModelFormatConfig("RDF/XML-ABBREV",
CONVERT, ModelSerializationFormat.N3, "RDF/XML-ABBREV", "application/rdf+xml" ),
new ModelFormatConfig("N3",
!CONVERT, ModelSerializationFormat.N3, NO_CONVERSION, "text/n3" ),
new ModelFormatConfig("N-TRIPLE",
!CONVERT, ModelSerializationFormat.NTRIPLE, NO_CONVERSION, "text/plain" ),
new ModelFormatConfig("TTL",
CONVERT, ModelSerializationFormat.N3, "TTL", "application/x-turtle" ),
new ModelFormatConfig("JSON-LD",
CONVERT, ModelSerializationFormat.N3, "JSON-LD", "application/javascript" ) };
public static class ModelFormatConfig extends FormatConfig{
public RDFService.ModelSerializationFormat wireFormat;
public String jenaResponseFormat;
public ModelFormatConfig( String valueFromForm,
boolean converstionFromWireFormat,
RDFService.ModelSerializationFormat wireFormat,
String jenaResponseFormat,
String responseMimeType){
this.valueFromForm = valueFromForm;
this.converstionFromWireFormat = converstionFromWireFormat;
this.wireFormat = wireFormat;
this.jenaResponseFormat = jenaResponseFormat;
this.responseMimeType = responseMimeType;
}
}
private static RSFormatConfig[] rsfs = {
new RSFormatConfig( "RS_XML",
!CONVERT, ResultFormat.XML, null, "text/xml"),
new RSFormatConfig( "RS_TEXT",
!CONVERT, ResultFormat.TEXT, null, "text/plain"),
new RSFormatConfig( "vitro:csv",
!CONVERT, ResultFormat.CSV, null, "text/csv"),
new RSFormatConfig( "RS_JSON",
!CONVERT, ResultFormat.JSON, null, "application/javascript") };
public static class RSFormatConfig extends FormatConfig{
public ResultFormat wireFormat;
public ResultSetFormat jenaResponseFormat;
public RSFormatConfig( String valueFromForm,
boolean converstionFromWireFormat,
ResultFormat wireFormat,
ResultSetFormat jenaResponseFormat,
String responseMimeType ){
this.valueFromForm = valueFromForm;
this.converstionFromWireFormat = converstionFromWireFormat;
this.wireFormat = wireFormat;
this.jenaResponseFormat = jenaResponseFormat;
this.responseMimeType = responseMimeType;
}
}
static{
HashMap<String, Float> map = new HashMap<String, Float>();
/* move the lists of configurations into maps for easy lookup
* by both MIME content type and the parameters from the form */
for( RSFormatConfig rsfc : rsfs ){
rsFormats.put( rsfc.valueFromForm, rsfc );
rsFormats.put( rsfc.responseMimeType, rsfc);
map.put(rsfc.responseMimeType, 1.0f);
}
for( ModelFormatConfig mfc : fmts ){
modelFormats.put( mfc.valueFromForm, mfc);
modelFormats.put(mfc.responseMimeType, mfc);
map.put(mfc.responseMimeType, 1.0f);
}
ACCEPTED_CONTENT_TYPES = Collections.unmodifiableMap(map);
}
/**
* Get the content type based on content negotiation.
* Returns null of no content type can be agreed on or
* if there is no accept header.
*/
protected String checkForContentType( String acceptHeader ) {
if (acceptHeader == null)
return null;
try {
Map<String, Float> typesAndQ = ContentType
.getTypesAndQ(acceptHeader);
String ctStr = ContentType
.getBestContentType(typesAndQ,ACCEPTED_CONTENT_TYPES);
if( ACCEPTED_CONTENT_TYPES.containsKey( ctStr )){
return ctStr;
}
} catch (Throwable th) {
log.error("Problem while checking accept header ", th);
}
return null;
}
}

View file

@ -0,0 +1,228 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.admin;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.hp.hpl.jena.query.Query;
import com.hp.hpl.jena.query.QueryParseException;
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
import edu.cornell.mannlib.vitro.webapp.beans.Ontology;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery.InvalidQueryTypeException;
import edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery.ResultSetMediaType;
import edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery.SparqlQueryApiExecutor;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
import edu.cornell.mannlib.vitro.webapp.utils.SparqlQueryUtils;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Present the SPARQL Query form, and execute the queries.
*/
public class SparqlQueryController extends FreemarkerHttpServlet {
private static final Log log = LogFactory
.getLog(SparqlQueryController.class);
private static final String TEMPLATE_NAME = "admin-sparqlQueryForm.ftl";
/**
* Always show these prefixes, even though they don't appear in the list of
* ontologies.
*/
private static final List<Prefix> DEFAULT_PREFIXES = buildDefaults();
private static List<Prefix> buildDefaults() {
Prefix[] array = new Prefix[] {
new Prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
new Prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
new Prefix("xsd", "http://www.w3.org/2001/XMLSchema#"),
new Prefix("owl", "http://www.w3.org/2002/07/owl#"),
new Prefix("swrl", "http://www.w3.org/2003/11/swrl#"),
new Prefix("swrlb", "http://www.w3.org/2003/11/swrlb#"),
new Prefix("vitro",
"http://vitro.mannlib.cornell.edu/ns/vitro/0.7#") };
return Collections.unmodifiableList(Arrays.asList(array));
}
private static final String[] SAMPLE_QUERY = { //
"", //
"#", //
"# This example query gets 20 geographic locations", //
"# and (if available) their labels", //
"#", //
"SELECT ?geoLocation ?label", //
"WHERE", //
"{", //
" ?geoLocation rdf:type vivo:GeographicLocation",
" OPTIONAL { ?geoLocation rdfs:label ?label } ", //
"}", //
"LIMIT 20" //
};
/**
* If a query has been provided, we answer it directly, bypassing the
* Freemarker mechanisms.
*/
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
if (!isAuthorizedToDisplayPage(req, resp,
SimplePermission.USE_SPARQL_QUERY_PAGE.ACTIONS)) {
return;
}
if (req.getParameterMap().containsKey("query")) {
respondToQuery(req, resp);
} else {
super.doGet(req, resp);
}
}
private void respondToQuery(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
RDFService rdfService = RDFServiceUtils.getRDFServiceFactory(
getServletContext()).getRDFService();
String queryString = req.getParameter("query");
try {
String format = interpretRequestedFormats(req, queryString);
SparqlQueryApiExecutor core = SparqlQueryApiExecutor.instance(
rdfService, queryString, format);
resp.setContentType(core.getMediaType());
core.executeAndFormat(resp.getOutputStream());
} catch (InvalidQueryTypeException e) {
do400BadRequest("Query type is not SELECT, ASK, CONSTRUCT, "
+ "or DESCRIBE: '" + queryString + "'", resp);
} catch (QueryParseException e) {
do400BadRequest("Failed to parse query: '" + queryString + "''", e,
resp);
} catch (NotAcceptableException | AcceptHeaderParsingException e) {
do500InternalServerError("Problem with the page fields: the "
+ "selected fields do not include an "
+ "acceptable content type.", e, resp);
} catch (RDFServiceException e) {
do500InternalServerError("Problem executing the query.", e, resp);
}
}
private String interpretRequestedFormats(HttpServletRequest req,
String queryString) throws NotAcceptableException {
Query query = SparqlQueryUtils.create(queryString);
String parameterName = (query.isSelectType() || query.isAskType()) ? "resultFormat"
: "rdfResultFormat";
String parameterValue = req.getParameter(parameterName);
if (StringUtils.isBlank(parameterValue)) {
throw new NotAcceptableException("Parameter '" + parameterName
+ "' was '" + parameterValue + "'.");
} else {
return parameterValue;
}
}
private void do400BadRequest(String message, HttpServletResponse resp)
throws IOException {
resp.setStatus(400);
resp.getWriter().println(message);
}
private void do400BadRequest(String message, Exception e,
HttpServletResponse resp) throws IOException {
resp.setStatus(400);
PrintWriter w = resp.getWriter();
w.println(message);
e.printStackTrace(w);
}
private void do500InternalServerError(String message, Exception e,
HttpServletResponse resp) throws IOException {
resp.setStatus(500);
PrintWriter w = resp.getWriter();
w.println(message);
e.printStackTrace(w);
}
@Override
protected ResponseValues processRequest(VitroRequest vreq) throws Exception {
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("sampleQuery", buildSampleQuery(buildPrefixList(vreq)));
bodyMap.put("title", "SPARQL Query");
bodyMap.put("submitUrl", UrlBuilder.getUrl("admin/sparqlquery"));
return new TemplateResponseValues(TEMPLATE_NAME, bodyMap);
}
private List<Prefix> buildPrefixList(VitroRequest vreq) {
List<Prefix> prefixList = new ArrayList<>(DEFAULT_PREFIXES);
OntologyDao dao = vreq.getUnfilteredWebappDaoFactory().getOntologyDao();
List<Ontology> ontologies = dao.getAllOntologies();
if (ontologies == null) {
ontologies = Collections.emptyList();
}
int unnamedOntologyIndex = 1;
for (Ontology ont : ontologies) {
String prefix = ont.getPrefix();
if (prefix == null) {
prefix = "p" + unnamedOntologyIndex++;
}
prefixList.add(new Prefix(prefix, ont.getURI()));
}
return prefixList;
}
private String buildSampleQuery(List<Prefix> prefixList) {
StringWriter sw = new StringWriter();
PrintWriter writer = new PrintWriter(sw);
for (Prefix p : prefixList) {
writer.println(p);
}
for (String line : SAMPLE_QUERY) {
writer.println(line);
}
return sw.toString();
}
public static class Prefix {
private final String prefix;
private final String uri;
public Prefix(String prefix, String uri) {
this.prefix = prefix;
this.uri = uri;
}
@Override
public String toString() {
return String.format("PREFIX %-9s <%s>", prefix + ":", uri);
}
}
}