Java tutorial
/* ******************************************************************** Licensed to Jasig under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. Jasig licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package org.bedework.util.dav; import org.bedework.util.http.BasicHttpClient; import org.bedework.util.logging.BwLogger; import org.bedework.util.logging.Logged; import org.bedework.util.misc.Util; import org.bedework.util.xml.XmlEmit; import org.bedework.util.xml.XmlEmit.NameSpace; import org.bedework.util.xml.XmlUtil; import org.bedework.util.xml.tagdefs.WebdavTags; import org.apache.http.Header; import org.apache.http.message.BasicHeader; import org.apache.http.protocol.HTTP; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.InputSource; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.io.StringWriter; import java.net.URI; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.servlet.http.HttpServletResponse; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; /** Helper for DAV interactions * * @author Mike Douglass douglm - rpi.edu */ public class DavUtil implements Logged, Serializable { /** */ public static final Header depth0 = new BasicHeader("depth", "0"); /** */ public static final Header depth1 = new BasicHeader("depth", "1"); /** */ public static final Header depthinf = new BasicHeader("depth", "infinity"); /** Added to each request */ protected List<Header> extraHeaders; private Set<String> nameSpaces = new TreeSet<>(); /** */ public DavUtil() { addNs(WebdavTags.namespace); } /** */ public DavUtil(final List<Header> extraHeaders) { this(); if (!Util.isEmpty(extraHeaders)) { this.extraHeaders = new ArrayList<>(extraHeaders); } } public void addNs(final String val) { nameSpaces.add(val); } /** Represents the child of a collection * * @author Mike Douglass */ public static class DavChild implements Comparable<DavChild> { /** The href */ public String uri; /** Always requested */ public String displayName; /** Always requested */ public boolean isCollection; /** Same order as supplied properties */ public List<DavProp> propVals = new ArrayList<>(); /* Extracted from returned resource types */ public List<QName> resourceTypes = new ArrayList<>(); /** */ public int status; /** * @param nm a QName * @return DavProp or null */ public DavProp findProp(final QName nm) { if (Util.isEmpty(propVals)) { return null; } for (DavProp dp : propVals) { if (nm.equals(dp.name)) { return dp; } } return null; } @Override public int compareTo(final DavChild that) { if (isCollection != that.isCollection) { if (!isCollection) { return -1; } return 1; } if (displayName == null) { return -1; } if (that.displayName == null) { return 1; } return displayName.compareTo(that.displayName); } } /** Represents a property * * @author Mike Douglass */ public static class DavProp { /** */ public QName name; /** */ public Element element; /** */ public int status; } /** partially parsed propstat response element * * @author douglm */ public static class PropstatElement { /** */ public Collection<Element> props; /** */ public int status; /** May be null */ public String responseDescription; } /** partially parsed multi-status response element * * @author douglm */ public static class MultiStatusResponseElement { /** */ public String href; /** */ public List<PropstatElement> propstats = new ArrayList<>(); /** May be null */ public String responseDescription; } /** partially parsed multi-status response * * @author douglm */ public static class MultiStatusResponse { /** */ public List<MultiStatusResponseElement> responses = new ArrayList<>(); /** May be null */ public String responseDescription; } /** * @param in input stream * @return Collection<DavChild> * @throws Throwable */ public MultiStatusResponse getMultiStatusResponse(final InputStream in) throws Throwable { MultiStatusResponse res = new MultiStatusResponse(); Document doc = parseContent(in); Element root = doc.getDocumentElement(); /* <!ELEMENT multistatus (response+, responsedescription?) > */ expect(root, WebdavTags.multistatus); Collection<Element> responses = getChildren(root); int count = 0; // validity for (Element resp : responses) { count++; if (XmlUtil.nodeMatches(resp, WebdavTags.responseDescription)) { // Has to be last if (responses.size() > count) { throw new Exception("Bad multstatus Expected " + "(response+, responsedescription?)"); } res.responseDescription = getElementContent(resp); continue; } if (!XmlUtil.nodeMatches(resp, WebdavTags.response)) { throw new Exception("Bad multstatus Expected " + "(response+, responsedescription?) found " + resp); } /* <!ELEMENT response (href, ((href*, status)|(propstat+)), responsedescription?) > */ MultiStatusResponseElement msre = new MultiStatusResponseElement(); res.responses.add(msre); Iterator<Element> elit = getChildren(resp).iterator(); Node nd = elit.next(); if (!XmlUtil.nodeMatches(nd, WebdavTags.href)) { throw new Exception("Bad response. Expected href found " + nd); } msre.href = getElementContent((Element) nd); while (elit.hasNext()) { nd = elit.next(); if (!XmlUtil.nodeMatches(nd, WebdavTags.propstat)) { throw new Exception("Bad response. Expected propstat found " + nd); } /* <!ELEMENT propstat (prop, status, responsedescription?) > */ PropstatElement pse = new PropstatElement(); msre.propstats.add(pse); Iterator<Element> propstatit = getChildren(nd).iterator(); Node propnd = propstatit.next(); if (!XmlUtil.nodeMatches(propnd, WebdavTags.prop)) { throw new Exception("Bad response. Expected prop found " + propnd); } if (!propstatit.hasNext()) { throw new Exception("Bad response. Expected propstat/status"); } pse.status = httpStatus(propstatit.next()); if (propstatit.hasNext()) { Node rdesc = propstatit.next(); if (!XmlUtil.nodeMatches(rdesc, WebdavTags.responseDescription)) { throw new Exception( "Bad response, expected null or " + "responsedescription. Found: " + rdesc); } pse.responseDescription = getElementContent(resp); } /* process each property with this status */ pse.props = getChildren(propnd); } } return res; } /** Do a synch report on the targeted collection. * * * @param cl http client * @param path of collection * @param syncToken from last report or null * @param props null for a default set * @return Collection of DavChild or null for not found * @throws Throwable */ public Collection<DavChild> syncReport(final BasicHttpClient cl, final String path, final String syncToken, final Collection<QName> props) throws Throwable { final StringWriter sw = new StringWriter(); final XmlEmit xml = getXml(); addNs(xml, WebdavTags.namespace); xml.startEmit(sw); /* <?xml version="1.0" encoding="utf-8" ?> <D:sync-collection xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> <D:sync-token/> <D:prop> <D:getetag/> </D:prop> </D:sync-collection> */ xml.openTag(WebdavTags.syncCollection); if (syncToken == null) { xml.emptyTag(WebdavTags.syncToken); } else { xml.property(WebdavTags.syncToken, syncToken); } xml.property(WebdavTags.synclevel, "1"); xml.openTag(WebdavTags.prop); xml.emptyTag(WebdavTags.getetag); if (props != null) { for (final QName pr : props) { if (pr.equals(WebdavTags.getetag)) { continue; } addNs(xml, pr.getNamespaceURI()); xml.emptyTag(pr); } } xml.closeTag(WebdavTags.prop); xml.closeTag(WebdavTags.syncCollection); final byte[] content = sw.toString().getBytes(); final int res = sendRequest(cl, "REPORT", path, depth0, "text/xml", // contentType, content.length, // contentLen, content); if (res == HttpServletResponse.SC_NOT_FOUND) { return null; } final int SC_MULTI_STATUS = 207; // not defined for some reason if (res != SC_MULTI_STATUS) { if (debug()) { debug("Got response " + res + " for path " + path); } //throw new Exception("Got response " + res + " for path " + path); return null; } final Document doc = parseContent(cl.getResponseBodyAsStream()); final Element root = doc.getDocumentElement(); /* <!ELEMENT multistatus (response+, responsedescription?) > */ expect(root, WebdavTags.multistatus); return processResponses(getChildren(root), null); } /** Return the DavChild element for the targeted node. * * @param cl http client * @param path of resource * @param props null for a default set * @return DavChild or null for not found * @throws Throwable */ public DavChild getProps(final BasicHttpClient cl, final String path, final Collection<QName> props) throws Throwable { return makeDavChild(propfind(cl, normalizePath(path), props, depth0)); } /** * @param cl the client * @param parentPath the path * @param props null for a default set * @return Collection<DavChild> - empty for no children - null for path not found. * @throws Throwable */ public Collection<DavChild> getChildrenUrls(final BasicHttpClient cl, final String parentPath, final Collection<QName> props) throws Throwable { final String path = normalizePath(parentPath); return processResponses(propfind(cl, path, props, depth1), new URI(path)); } /** * @param cl the client * @param path to resource * @param props null for a default set * @param depthHeader to set depth of operation * @return List<Element> from multi-status response * @throws Throwable */ public List<Element> propfind(final BasicHttpClient cl, final String path, final Collection<QName> props, final Header depthHeader) throws Throwable { final StringWriter sw = new StringWriter(); final XmlEmit xml = getXml(); addNs(xml, WebdavTags.namespace); xml.startEmit(sw); xml.openTag(WebdavTags.propfind); xml.openTag(WebdavTags.prop); xml.emptyTag(WebdavTags.displayname); xml.emptyTag(WebdavTags.resourcetype); if (props != null) { for (final QName pr : props) { if (pr.equals(WebdavTags.displayname)) { continue; } if (pr.equals(WebdavTags.resourcetype)) { continue; } addNs(xml, pr.getNamespaceURI()); xml.emptyTag(pr); } } xml.closeTag(WebdavTags.prop); xml.closeTag(WebdavTags.propfind); final byte[] content = sw.toString().getBytes(); final int res = sendRequest(cl, "PROPFIND", path, depthHeader, "text/xml", // contentType, content.length, // contentLen, content); if (res == HttpServletResponse.SC_NOT_FOUND) { return null; } final int SC_MULTI_STATUS = 207; // not defined for some reason if (res != SC_MULTI_STATUS) { if (debug()) { debug("Got response " + res + " for path " + path); } //throw new Exception("Got response " + res + " for path " + path); return null; } final Document doc = parseContent(cl.getResponseBodyAsStream()); final Element root = doc.getDocumentElement(); /* <!ELEMENT multistatus (response+, responsedescription?) > */ expect(root, WebdavTags.multistatus); return getChildren(root); } /** * @param cl the client * @param methodName * @param url * @param header * @param contentType * @param contentLen * @param content * @return * @throws Throwable */ public int sendRequest(final BasicHttpClient cl, final String methodName, final String url, final Header header, final String contentType, final int contentLen, final byte[] content) throws Throwable { int hdrSize = 0; if (header != null) { hdrSize = 1; } if (!Util.isEmpty(extraHeaders)) { hdrSize += extraHeaders.size(); } List<Header> hdrs = null; if (hdrSize > 0) { hdrs = new ArrayList<>(hdrSize); if (header != null) { hdrs.add(header); } if (!Util.isEmpty(extraHeaders)) { hdrs.addAll(extraHeaders); } } return cl.sendRequest(methodName, url, hdrs, contentType, contentLen, content); } /* ==================================================================== * XmlUtil wrappers * ==================================================================== */ protected XmlEmit getXml() throws Throwable { final XmlEmit xml = new XmlEmit(); for (final String ns : nameSpaces) { addNs(xml, ns); } return xml; } /** Add a namespace * * @param val * @throws Throwable */ protected void addNs(final XmlEmit xml, final String val) throws Throwable { if (xml.getNameSpace(val) == null) { xml.addNs(new NameSpace(val, null), false); } } /** Parse the content, and return the DOM representation. * * @param in content as stream * @return Document Parsed body or null for no body * @exception Throwable Some error occurred. */ protected Document parseContent(final InputStream in) throws Throwable { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.parse(new InputSource(new InputStreamReader(in))); } /** Parse a DAV error response * * @param in response * @return a single error element or null */ public Element parseError(final InputStream in) { try { final Document doc = parseContent(in); final Element root = doc.getDocumentElement(); expect(root, WebdavTags.error); final List<Element> els = getChildren(root); if (els.size() != 1) { return null; } return els.get(0); } catch (final Throwable ignored) { return null; } } /** * @param nd * @return List<Element> * @throws Throwable */ public static List<Element> getChildren(final Node nd) throws Throwable { try { return XmlUtil.getElements(nd); } catch (final Throwable t) { //if (debug) { // getLogger().error(this, t); //} throw new Exception(t.getMessage()); } } /** * @param nd * @return Element[] * @throws Throwable */ public static Element[] getChildrenArray(final Node nd) throws Throwable { try { return XmlUtil.getElementsArray(nd); } catch (Throwable t) { //if (debug) { // getLogger().error(this, t); //} throw new Exception(t.getMessage()); } } /** * @param nd * @return Element * @throws Throwable */ public static Element getOnlyChild(final Node nd) throws Throwable { try { return XmlUtil.getOnlyElement(nd); } catch (Throwable t) { //if (debug) { // getLogger().error(this, t); //} throw new Exception(t.getMessage()); } } /** * @param el * @return String * @throws Throwable */ public static String getElementContent(final Element el) throws Throwable { try { return XmlUtil.getElementContent(el); } catch (Throwable t) { //if (debug) { // getLogger().error(this, t); //} throw new Exception(t.getMessage()); } } /** * @param el * @return boolean * @throws Throwable */ public static boolean isEmpty(final Element el) throws Throwable { try { return XmlUtil.isEmpty(el); } catch (Throwable t) { //if (debug) { // getLogger().error(this, t); //} throw new Exception(t.getMessage()); } } /** * @param el * @param tag * @throws Throwable */ public static void expect(final Element el, final QName tag) throws Throwable { if (!XmlUtil.nodeMatches(el, tag)) { throw new Exception("Expected " + tag); } } /** * @param el - must be status element * @return int status * @throws Throwable */ public static int httpStatus(final Element el) throws Throwable { if (!XmlUtil.nodeMatches(el, WebdavTags.status)) { throw new Exception("Bad response. Expected status found " + el); } final String s = getElementContent(el); if (s == null) { throw new Exception("Bad http status. Found null"); } try { final int start = s.indexOf(" "); final int end = s.indexOf(" ", start + 1); if (end < 0) { return Integer.valueOf(s.substring(start)); } return Integer.valueOf(s.substring(start + 1, end)); } catch (final Throwable t) { throw new Exception("Bad http status. Found " + s); } } /* private static class Response implements DavResp { // private boolean debug; DavIo client; Response(final DavIo client, final boolean debug) throws DavioException { this.client = client; // this.debug = debug; } @Override public int getRespCode() throws DavioException { return client.getStatusCode(); } @Override public String getContentType() throws DavioException { return client.getResponseContentType(); } @Override public long getContentLength() throws DavioException { return client.getResponseContentLength(); } @Override public String getCharset() throws DavioException { return client.getResponseCharSet(); } @Override public InputStream getContentStream() throws DavioException { return client.getResponseBodyAsStream(); } @Override public String getResponseBodyAsString() throws DavioException { return client.getResponseBodyAsString(); } @Override public Header getResponseHeader(final String name) throws DavioException { return client.getResponseHeader(name); } @Override public String getResponseHeaderValue(final String name) throws DavioException { return client.getResponseHeaderValue(name); } @Override public void close() { try { client.release(); } catch (Throwable t) { //error(t) } } } */ /** * @param responses null for a default set * @param parentURI null or uri of collection to excluse response * @return Collection<DavChild> - empty for no children - null for path not found. * @throws Throwable */ public Collection<DavChild> processResponses(final Collection<Element> responses, final URI parentURI) throws Throwable { if (responses == null) { return null; } final Collection<DavChild> result = new ArrayList<>(); int count = 0; // validity for (final Element resp : responses) { count++; if (XmlUtil.nodeMatches(resp, WebdavTags.responseDescription)) { // Has to be last if (responses.size() > count) { throw new Exception("Bad multstatus Expected " + "(response+, responsedescription?)"); } continue; } if (XmlUtil.nodeMatches(resp, WebdavTags.syncToken)) { // We did a sync report. Add the token to the list DavChild dc = new DavChild(); DavProp dp = new DavProp(); dp.name = WebdavTags.syncToken; dp.element = resp; dp.status = HttpServletResponse.SC_OK; dc.propVals.add(dp); dc.status = HttpServletResponse.SC_OK; result.add(dc); continue; } if (!XmlUtil.nodeMatches(resp, WebdavTags.response)) { throw new Exception("Bad multstatus Expected " + "(response+, responsedescription?) found " + resp); } DavChild dc = makeDavResponse(resp); /* We get the collection back as well - check for it and skip it. */ URI childURI = new URI(dc.uri); if ((parentURI != null) && parentURI.getPath().equals(childURI.getPath())) { continue; } result.add(dc); } return result; } /** Return the DavChild element for the targeted node. * * @param responseElements null for a default set * @return DavChild or null for not found * @throws Throwable */ private DavChild makeDavChild(final Collection<Element> responseElements) throws Throwable { if (responseElements == null) { // status 400 return null; } DavChild dc = null; int count = 0; // validity for (Element resp : responseElements) { count++; if (XmlUtil.nodeMatches(resp, WebdavTags.responseDescription)) { // Has to be last if (responseElements.size() > count) { throw new Exception("Bad multstatus Expected " + "(response+, responsedescription?)"); } continue; } if (!XmlUtil.nodeMatches(resp, WebdavTags.response)) { throw new Exception("Bad multstatus Expected " + "(response+, responsedescription?) found " + resp); } if (dc != null) { throw new Exception("Bad multstatus Expected only 1 response"); } dc = makeDavResponse(resp); } return dc; } private DavChild makeDavResponse(final Element resp) throws Throwable { /* <!ELEMENT response (href, ((href*, status)|(propstat+)), responsedescription?) > */ final Iterator<Element> elit = getChildren(resp).iterator(); Node nd = elit.next(); final DavChild dc = new DavChild(); if (!XmlUtil.nodeMatches(nd, WebdavTags.href)) { throw new Exception("Bad response. Expected href found " + nd); } dc.uri = URLDecoder.decode(getElementContent((Element) nd), HTTP.UTF_8); // href should be escaped while (elit.hasNext()) { nd = elit.next(); if (XmlUtil.nodeMatches(nd, WebdavTags.status)) { dc.status = httpStatus((Element) nd); continue; } dc.status = HttpServletResponse.SC_OK; if (!XmlUtil.nodeMatches(nd, WebdavTags.propstat)) { throw new Exception("Bad response. Expected propstat found " + nd); } /* <!ELEMENT propstat (prop, status, responsedescription?) > */ Iterator<Element> propstatit = getChildren(nd).iterator(); Node propnd = propstatit.next(); if (!XmlUtil.nodeMatches(propnd, WebdavTags.prop)) { throw new Exception("Bad response. Expected prop found " + propnd); } if (!propstatit.hasNext()) { throw new Exception("Bad response. Expected propstat/status"); } final int st = httpStatus(propstatit.next()); if (propstatit.hasNext()) { Node rdesc = propstatit.next(); if (!XmlUtil.nodeMatches(rdesc, WebdavTags.responseDescription)) { throw new Exception("Bad response, expected null or " + "responsedescription. Found: " + rdesc); } } /* process each property with this status */ Collection<Element> respProps = getChildren(propnd); for (Element pr : respProps) { /* XXX This needs fixing to handle content that is xml */ if (XmlUtil.nodeMatches(pr, WebdavTags.resourcetype)) { Collection<Element> rtypeProps = getChildren(pr); for (Element rtpr : rtypeProps) { if (XmlUtil.nodeMatches(rtpr, WebdavTags.collection)) { dc.isCollection = true; } dc.resourceTypes.add(XmlUtil.fromNode(rtpr)); } } else { DavProp dp = new DavProp(); dc.propVals.add(dp); dp.name = new QName(pr.getNamespaceURI(), pr.getLocalName()); dp.status = st; dp.element = pr; if (XmlUtil.nodeMatches(pr, WebdavTags.displayname)) { dc.displayName = getElementContent(pr); } } } } return dc; } private String normalizePath(final String path) { if (!path.endsWith("/")) { return path + "/"; } return path; } /* ==================================================================== * Logged methods * ==================================================================== */ private BwLogger logger = new BwLogger(); @Override public BwLogger getLogger() { if ((logger.getLoggedClass() == null) && (logger.getLoggedName() == null)) { logger.setLoggedClass(getClass()); } return logger; } }