diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 481229840..325fcf30a 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -91,6 +91,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [Internet of Things - Discovery](iot.md) | [XEP-0347](http://xmpp.org/extensions/xep-0347.html) | Describes how Things can be installed and discovered by their owners. | | Client State Indication | [XEP-0352](http://xmpp.org/extensions/xep-0352.html) | A way for the client to indicate its active/inactive state. | | [Push Notifications](pushnotifications.md) | [XEP-0357](http://xmpp.org/extensions/xep-0357.html) | Defines a way to manage push notifications from an XMPP Server. | +| HTTP File Upload | [XEP-0363](http://xmpp.org/extensions/xep-0363.html) | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. | | [Multi-User Chat Light](muclight.md) | [XEP-xxxx](http://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | Multi-User Chats for mobile XMPP applications and specific enviroment. | | Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers | diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java new file mode 100644 index 000000000..dfcc1f790 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java @@ -0,0 +1,375 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload; + +import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; +import org.jivesoftware.smackx.httpfileupload.element.Slot; +import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; +import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.packet.DataForm; +import org.jxmpp.jid.DomainBareJid; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * HTTP File Upload manager class. + * XEP version 0.2.5 + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public final class HttpFileUploadManager extends Manager { + + private static final Logger LOGGER = Logger.getLogger(HttpFileUploadManager.class.getName()); + + static { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + getInstanceFor(connection); + } + }); + } + + private static final Map INSTANCES = new WeakHashMap<>(); + private DomainBareJid defaultUploadService; + private Long maxFileSize; + + /** + * Obtain the HttpFileUploadManager responsible for a connection. + * + * @param connection the connection object. + * @return a HttpFileUploadManager instance + */ + public static synchronized HttpFileUploadManager getInstanceFor(XMPPConnection connection) { + HttpFileUploadManager httpFileUploadManager = INSTANCES.get(connection); + + if (httpFileUploadManager == null) { + httpFileUploadManager = new HttpFileUploadManager(connection); + INSTANCES.put(connection, httpFileUploadManager); + } + + return httpFileUploadManager; + } + + private HttpFileUploadManager(XMPPConnection connection) { + super(connection); + + connection.addConnectionListener(new AbstractConnectionListener() { + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + // No need to reset the cache if the connection got resumed. + if (resumed) { + return; + } + + try { + discoverUploadService(); + } catch (XMPPException.XMPPErrorException | SmackException.NotConnectedException + | SmackException.NoResponseException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Error during discovering HTTP File Upload service", e); + } + } + }); + } + + /** + * Discover upload service. + * + * Called automatically when connection is authenticated. + * + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @return true if upload service was discovered + + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + public boolean discoverUploadService() throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, + InterruptedException, SmackException.NoResponseException { + defaultUploadService = null; + maxFileSize = null; + + List servicesDiscoverInfo = ServiceDiscoveryManager.getInstanceFor(connection()) + .findServicesDiscoverInfo(SlotRequest.NAMESPACE, true, false); + + if (servicesDiscoverInfo.isEmpty()) { + return false; + } + + DiscoverInfo discoverInfo = servicesDiscoverInfo.get(0); + + defaultUploadService = discoverInfo.getFrom().asDomainBareJid(); + + if (defaultUploadService == null) { + return false; + } + + DataForm dataForm = DataForm.from(discoverInfo); + if (dataForm == null) { + return true; + } + + FormField field = dataForm.getField("max-file-size"); + if (field == null) { + return true; + } + + List values = field.getValues(); + if (!values.isEmpty()) { + maxFileSize = Long.valueOf(values.get(0)); + } + + return true; + } + + /** + * Check if upload service was discovered. + * + * @return true if upload service was discovered + */ + public boolean isUploadServiceDiscovered() { + return defaultUploadService != null; + } + + /** + * Get default upload service if it was discovered. + * + * @return upload service JID or null if not available + */ + public DomainBareJid getDefaultUploadService() { + return defaultUploadService; + } + + /** + * Get max file size allowed by upload service. + * + * @return max file size in bytes or null if not available + */ + public Long getMaxFileSize() { + return maxFileSize; + } + + /** + * Request slot and uploaded file to HTTP file upload service. + * + * You don't need to request slot and upload file separately, this method will do both. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param file file to be uploaded + * @return public URL for sharing uploaded file + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException + * @throws IOException in case of HTTP upload errors + */ + public URL uploadFile(File file) throws InterruptedException, XMPPException.XMPPErrorException, + SmackException, IOException { + return uploadFile(file, null); + } + + /** + * Callback interface to get upload progress. + */ + public interface UploadProgressListener { + /** + * Callback for displaying upload progress. + * + * @param uploadedBytes - number of bytes uploaded at the moment + * @param totalBytes - total number of bytes to be uploaded + */ + void onUploadProgress(long uploadedBytes, long totalBytes); + } + + /** + * Request slot and uploaded file to HTTP file upload service with progress callback. + * + * You don't need to request slot and upload file separately, this method will do both. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param file file to be uploaded + * @param listener upload progress listener of null + * @return public URL for sharing uploaded file + * + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException + * @throws IOException + */ + public URL uploadFile(File file, UploadProgressListener listener) throws InterruptedException, + XMPPException.XMPPErrorException, SmackException, IOException { + final Slot slot = requestSlot(file.getName(), file.length(), "application/octet-stream"); + + uploadFile(file, slot.getPutUrl(), listener); + + return slot.getGetUrl(); + } + + + /** + * Request a new upload slot from default upload service (if discovered). + * + * When you get slot you should upload file to PUT URL and share GET URL. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param filename name of file to be uploaded + * @param fileSize file size in bytes -- must be less or equal + * to {@link HttpFileUploadManager#getMaxFileSize()} (if available) + * @return file upload Slot in case of success + + * @throws IllegalArgumentException if fileSize is less than or equal to zero + * or greater than {@link HttpFileUploadManager#getMaxFileSize()} + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + public Slot requestSlot(String filename, long fileSize) throws InterruptedException, + XMPPException.XMPPErrorException, SmackException { + return requestSlot(filename, fileSize, null, null); + } + + /** + * Request a new upload slot with optional content type from default upload service (if discovered). + * + * When you get slot you should upload file to PUT URL and share GET URL. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param filename name of file to be uploaded + * @param fileSize file size in bytes -- must be less or equal + * to {@link HttpFileUploadManager#getMaxFileSize()} (if available) + * @param contentType file content-type or null + * @return file upload Slot in case of success + + * @throws IllegalArgumentException if fileSize is less than or equal to zero + * or greater than {@link HttpFileUploadManager#getMaxFileSize()} + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NoResponseException + */ + public Slot requestSlot(String filename, long fileSize, String contentType) throws SmackException, + InterruptedException, XMPPException.XMPPErrorException { + return requestSlot(filename, fileSize, contentType, null); + } + + /** + * Request a new upload slot with optional content type from custom upload service. + * + * When you get slot you should upload file to PUT URL and share GET URL. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param filename name of file to be uploaded + * @param fileSize file size in bytes -- must be less or equal + * to {@link HttpFileUploadManager#getMaxFileSize()} (if available) + * @param contentType file content-type or null + * @param uploadService upload service to use or null for default one + * @return file upload Slot in case of success + * @throws IllegalArgumentException if fileSize is less than or equal to zero + * or greater than {@link HttpFileUploadManager#getMaxFileSize()} + * @throws SmackException + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + */ + public Slot requestSlot(String filename, long fileSize, String contentType, DomainBareJid uploadService) + throws SmackException, InterruptedException, XMPPException.XMPPErrorException { + if (defaultUploadService == null && uploadService == null) { + throw new SmackException("No upload service specified or discovered."); + } + + if (uploadService == null && maxFileSize != null) { + if (fileSize > maxFileSize) { + throw new IllegalArgumentException("Requested file size " + fileSize + + " is greater than max allowed size " + maxFileSize); + } + } + + SlotRequest slotRequest = new SlotRequest(filename, fileSize, contentType); + if (uploadService != null) { + slotRequest.setTo(uploadService); + } else { + slotRequest.setTo(defaultUploadService); + } + + return connection().createStanzaCollectorAndSend(slotRequest).nextResultOrThrow(); + } + + private void uploadFile(File file, URL putUrl, UploadProgressListener listener) throws IOException { + final HttpURLConnection urlConnection = (HttpURLConnection) putUrl.openConnection(); + urlConnection.setRequestMethod("PUT"); + urlConnection.setUseCaches(false); + urlConnection.setDoOutput(true); + urlConnection.setRequestProperty("Content-Type", "application/octet-stream;"); + OutputStream outputStream = urlConnection.getOutputStream(); + + long bytesSend = 0; + + long fileSize = file.length(); + if (listener != null) { + listener.onUploadProgress(0, fileSize); + } + + BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + outputStream.flush(); + bytesSend += bytesRead; + + if (listener != null) { + listener.onUploadProgress(bytesSend, fileSize); + } + + } + + inputStream.close(); + outputStream.close(); + + int status = urlConnection.getResponseCode(); + if (status != HttpURLConnection.HTTP_CREATED) { + throw new IOException("Error response from server during file upload:" + + " " + urlConnection.getResponseCode() + + " " + urlConnection.getResponseMessage() + + ", file size: " + fileSize + + ", put URL: " + putUrl); + } + } + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java new file mode 100644 index 000000000..57eec5d62 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java @@ -0,0 +1,70 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.XmlStringBuilder; + +/** + * File Too Large error extension. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class FileTooLargeError implements ExtensionElement { + public static final String ELEMENT = "file-too-large"; + public static final String NAMESPACE = SlotRequest.NAMESPACE; + + private final long maxFileSize; + + public FileTooLargeError(long maxFileSize) { + this.maxFileSize = maxFileSize; + } + + public long getMaxFileSize() { + return maxFileSize; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + xml.element("max-file-size", String.valueOf(maxFileSize)); + xml.closeElement(ELEMENT); + return xml; + } + + public static FileTooLargeError from(IQ iq) { + XMPPError error = iq.getError(); + if (error == null) { + return null; + } + return error.getExtension(ELEMENT, NAMESPACE); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java new file mode 100644 index 000000000..470987dc4 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java @@ -0,0 +1,62 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload.element; + +import org.jivesoftware.smack.packet.IQ; + +import java.net.URL; + +/** + * Slot responded by upload service. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class Slot extends IQ { + + public static final String ELEMENT = "slot"; + public static final String NAMESPACE = SlotRequest.NAMESPACE; + + private final URL putUrl; + private final URL getUrl; + + public Slot(URL putUrl, URL getUrl) { + super(ELEMENT, NAMESPACE); + setType(Type.result); + this.putUrl = putUrl; + this.getUrl = getUrl; + } + + public URL getPutUrl() { + return putUrl; + } + + public URL getGetUrl() { + return getUrl; + } + + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + + xml.element("put", putUrl.toString()); + xml.element("get", getUrl.toString()); + + return xml; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java new file mode 100644 index 000000000..bcdb823e6 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java @@ -0,0 +1,84 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload.element; + +import org.jivesoftware.smack.packet.IQ; + +/** + * Upload slot request. + + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class SlotRequest extends IQ { + public static final String ELEMENT = "request"; + public static final String NAMESPACE = "urn:xmpp:http:upload"; + + private final String filename; + private final long size; + private final String contentType; + + + /** + * Create new slot request. + * + * @throws IllegalArgumentException if size is less than or equal to zero + * @param filename name of file + * @param size size of file in bytes + * @param contentType file content type or null + */ + public SlotRequest(String filename, long size, String contentType) { + super(ELEMENT, NAMESPACE); + + if (size <= 0) { + throw new IllegalArgumentException("File fileSize must be greater than zero."); + } + + this.filename = filename; + this.size = size; + this.contentType = contentType; + + setType(Type.get); + } + + public SlotRequest(String filename, long size) { + this(filename, size, null); + } + + public String getFilename() { + return filename; + } + + public long getSize() { + return size; + } + + public String getContentType() { + return contentType; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + xml.element("filename", filename); + xml.element("size", String.valueOf(size)); + if (contentType != null) { + xml.element("content-type", contentType); + } + return xml; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/package-info.java new file mode 100644 index 000000000..d20c3658b --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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. + */ + +/** + * IQ stanzas and extensions for XEP-0363: HTTP File Upload. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +package org.jivesoftware.smackx.httpfileupload.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java new file mode 100644 index 000000000..d61996ae9 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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. + */ + +/** + * Smack's API for XEP-0363: HTTP File Upload. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +package org.jivesoftware.smackx.httpfileupload; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java new file mode 100644 index 000000000..3f1accb7c --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java @@ -0,0 +1,57 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload.provider; + +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; +import org.xmlpull.v1.XmlPullParser; + +/** + * Provider for File Too Large error extension. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class FileTooLargeErrorProvider extends ExtensionElementProvider { + + @Override + public FileTooLargeError parse(XmlPullParser parser, int initialDepth) throws Exception { + Long maxFileSize = null; + + outerloop: while(true) { + int event = parser.next(); + + switch (event) { + case XmlPullParser.START_TAG: + String name = parser.getName(); + switch(name) { + case "max-file-size": + maxFileSize = Long.valueOf(parser.nextText()); + break; + } + break; + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + } + } + + return new FileTooLargeError(maxFileSize); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProvider.java new file mode 100644 index 000000000..a30eb9330 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProvider.java @@ -0,0 +1,66 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload.provider; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.httpfileupload.element.Slot; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.net.URL; + +/** + * Provider for Slot. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class SlotProvider extends IQProvider { + + @Override + public Slot parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException { + URL putUrl = null; + URL getUrl = null; + + outerloop: while (true) { + int event = parser.next(); + + switch (event) { + case XmlPullParser.START_TAG: + String name = parser.getName(); + switch(name) { + case "put": + putUrl = new URL(parser.nextText()); + break; + case "get": + getUrl = new URL(parser.nextText()); + break; + } + break; + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + } + } + + return new Slot(putUrl, getUrl); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/package-info.java new file mode 100644 index 000000000..3894df689 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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. + */ + +/** + * Providers for XEP-0363: HTTP File Upload. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +package org.jivesoftware.smackx.httpfileupload.provider; diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 55b89911d..0b9f7cf8a 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -232,5 +232,17 @@ urn:xmpp:chat-markers:0 org.jivesoftware.smackx.chat_markers.provider.AcknowledgedProvider - + + + + slot + urn:xmpp:http:upload + org.jivesoftware.smackx.httpfileupload.provider.SlotProvider + + + file-too-large + urn:xmpp:http:upload + org.jivesoftware.smackx.httpfileupload.provider.FileTooLargeErrorProvider + + diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml index c621a0d43..05b819a27 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml @@ -4,5 +4,6 @@ org.jivesoftware.smackx.iot.data.IoTDataManager org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager + org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java new file mode 100644 index 000000000..40c79f335 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java @@ -0,0 +1,39 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload; + + +import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; +import org.junit.Assert; +import org.junit.Test; + +public class FileTooLargeErrorCreateTest { + String fileTooLargeErrorExtensionExample + = "" + + "20000" + + ""; + + @Test + public void checkFileTooLargeErrorExtensionCreation() { + FileTooLargeError fileTooLargeError = new FileTooLargeError(20000); + + Assert.assertEquals(20000, fileTooLargeError.getMaxFileSize()); + Assert.assertEquals(fileTooLargeErrorExtensionExample, fileTooLargeError.toXML().toString()); + + } + +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorProviderTest.java new file mode 100644 index 000000000..10573d938 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorProviderTest.java @@ -0,0 +1,58 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; +import org.junit.Assert; +import org.junit.Test; + +public class FileTooLargeErrorProviderTest { + + /** + * Example 7. Alternative response by the upload service if the file size was too large + * @see XEP-0363: HTTP File Upload 5. Error conditions + */ + String slotErrorFileToLarge + = "" + + "" + + "my_juliet.png" + + "23456" + + "" + + "" + + "" + + "File too large. The maximum file size is 20000 bytes" + + "" + + "20000" + + "" + + "" + + ""; + + @Test + public void checkSlotErrorFileToLarge() throws Exception { + IQ fileTooLargeErrorIQ = PacketParserUtils.parseStanza(slotErrorFileToLarge); + + Assert.assertEquals(IQ.Type.error, fileTooLargeErrorIQ.getType()); + + FileTooLargeError fileTooLargeError = FileTooLargeError.from(fileTooLargeErrorIQ); + Assert.assertEquals(20000, fileTooLargeError.getMaxFileSize()); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java new file mode 100644 index 000000000..838de9466 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java @@ -0,0 +1,46 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload; + + +import org.jivesoftware.smackx.httpfileupload.element.Slot; +import org.junit.Assert; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.net.URL; + +public class SlotCreateTest { + String testSlot + = "" + + "https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + + ""; + + @Test + public void checkSlotRequestCreation() throws MalformedURLException { + Slot slot = new Slot(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + new URL("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png")); + + Assert.assertEquals(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + slot.getPutUrl()); + Assert.assertEquals(new URL("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + slot.getGetUrl()); + + Assert.assertEquals(testSlot, slot.getChildElementXML().toString()); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotProviderTest.java new file mode 100644 index 000000000..13b830b67 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotProviderTest.java @@ -0,0 +1,58 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.httpfileupload.element.Slot; +import org.jivesoftware.smackx.httpfileupload.provider.SlotProvider; +import org.junit.Assert; +import org.junit.Test; +import org.xmlpull.v1.XmlPullParser; + +import java.net.URL; + + +public class SlotProviderTest { + + /** + * Example 6. The upload service responds with a slot + * @see XEP-0363: HTTP File Upload 4. Requesting a slot + */ + String slotExample + = "" + + "" + + "https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + + "" + + ""; + + @Test + public void checkSlotProvider() throws Exception { + XmlPullParser parser = PacketParserUtils.getParserFor(slotExample); + Slot slot = new SlotProvider().parse(parser); + + Assert.assertEquals(IQ.Type.result, slot.getType()); + Assert.assertEquals(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + slot.getPutUrl()); + Assert.assertEquals(new URL("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + slot.getGetUrl()); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java new file mode 100644 index 000000000..e7ef98bb1 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java @@ -0,0 +1,71 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * Licensed 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.jivesoftware.smackx.httpfileupload; + +import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; +import org.junit.Assert; +import org.junit.Test; +import org.jxmpp.stringprep.XmppStringprepException; + + +public class SlotRequestCreateTest { + + String testRequest + = "" + + "my_juliet.png" + + "23456" + + "image/jpeg" + + ""; + + String testRequestWithoutContentType + = "" + + "my_romeo.png" + + "52523" + + ""; + + @Test + public void checkSlotRequestCreation() throws XmppStringprepException { + SlotRequest slotRequest = new SlotRequest("my_juliet.png", 23456, "image/jpeg"); + + Assert.assertEquals("my_juliet.png", slotRequest.getFilename()); + Assert.assertEquals(23456, slotRequest.getSize()); + Assert.assertEquals("image/jpeg", slotRequest.getContentType()); + + Assert.assertEquals(testRequest, slotRequest.getChildElementXML().toString()); + } + + @Test + public void checkSlotRequestCreationWithoutContentType() throws XmppStringprepException { + SlotRequest slotRequest = new SlotRequest("my_romeo.png", 52523); + + Assert.assertEquals("my_romeo.png", slotRequest.getFilename()); + Assert.assertEquals(52523, slotRequest.getSize()); + Assert.assertEquals(null, slotRequest.getContentType()); + + Assert.assertEquals(testRequestWithoutContentType, slotRequest.getChildElementXML().toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void checkSlotRequestCreationNegativeSize() { + new SlotRequest("my_juliet.png", -23456, "image/jpeg"); + } + + @Test(expected = IllegalArgumentException.class) + public void checkSlotRequestCreationZeroSize() { + new SlotRequest("my_juliet.png", 0, "image/jpeg"); + } +}