diff options
Diffstat (limited to '')
5 files changed, 846 insertions, 0 deletions
diff --git a/scripting/java/com/sun/star/script/framework/io/UCBStreamHandler.java b/scripting/java/com/sun/star/script/framework/io/UCBStreamHandler.java new file mode 100644 index 000000000..6413745d3 --- /dev/null +++ b/scripting/java/com/sun/star/script/framework/io/UCBStreamHandler.java @@ -0,0 +1,275 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF 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 . + */ + +package com.sun.star.script.framework.io; + +import com.sun.star.io.XInputStream; +import com.sun.star.io.XOutputStream; +import com.sun.star.io.XTruncate; + +import com.sun.star.script.framework.log.LogUtils; +import com.sun.star.script.framework.provider.PathUtils; + +import com.sun.star.ucb.XSimpleFileAccess; + +import com.sun.star.uno.UnoRuntime; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import java.util.HashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class UCBStreamHandler extends URLStreamHandler { + + public static final String separator = "/ucb/"; + + private final XSimpleFileAccess m_xSimpleFileAccess; + private final HashMap<String, InputStream> m_jarStreamMap = new HashMap<String, InputStream>(12); + private static String m_ucbscheme; + + public UCBStreamHandler(String scheme, XSimpleFileAccess xSFA) { + LogUtils.DEBUG("UCBStreamHandler ctor, scheme = " + scheme); + UCBStreamHandler.m_ucbscheme = scheme; + this.m_xSimpleFileAccess = xSFA; + } + + @Override + public void parseURL(URL url, String spec, int start, int limit) { + + LogUtils.DEBUG("**XUCBStreamHandler, parseURL: " + url + " spec: " + + spec + " start: " + start + " limit: " + limit); + + String file = url.getFile(); + + if (file == null) + file = spec.substring(start, limit); + else + file += spec.substring(start, limit); + + LogUtils.DEBUG("**For scheme = " + m_ucbscheme); + LogUtils.DEBUG("**Setting path = " + file); + setURL(url, m_ucbscheme, null, -1, null, null, file, null, null); + } + + @Override + public URLConnection openConnection(URL u) throws IOException { + return new UCBConnection(u); + } + + private class UCBConnection extends URLConnection { + + public UCBConnection(URL url) { + super(url); + } + + @Override + public void connect() { + } + + @Override + public InputStream getInputStream() throws IOException { + LogUtils.DEBUG("UCBConnectionHandler GetInputStream on " + url); + String sUrl = url.toString(); + + if (sUrl.lastIndexOf(separator) == -1) { + LogUtils.DEBUG("getInputStream straight file load"); + return getFileStreamFromUCB(sUrl); + } else { + String path = sUrl.substring(0, sUrl.lastIndexOf(separator)); + + String file = + sUrl.substring(sUrl.lastIndexOf(separator) + separator.length()); + + LogUtils.DEBUG("getInputStream, load of file from another file eg. " + + file + " from " + path); + + return getUCBStream(file, path); + } + } + @Override + public OutputStream getOutputStream() throws IOException { + LogUtils.DEBUG("UCBConnectionHandler getOutputStream on " + url); + OutputStream os = null; + + try { + String sUrl = url.toString(); + + if (sUrl.lastIndexOf(separator) != -1) { + String path = sUrl.substring(0, sUrl.lastIndexOf(separator)); + + if (m_xSimpleFileAccess.isReadOnly(path)) { + throw new java.io.IOException("File is read only"); + } + + LogUtils.DEBUG("getOutputStream, create o/p stream for file eg. " + + path); + + // we will only deal with simple file write + XOutputStream xos = m_xSimpleFileAccess.openFileWrite(path); + + XTruncate xtrunc = + UnoRuntime.queryInterface(XTruncate.class, xos); + + if (xtrunc != null) { + xtrunc.truncate(); + } + + os = new XOutputStreamWrapper(xos); + } + + if (os == null) { + throw new IOException("Failed to get OutputStream for " + + sUrl); + } + } catch (com.sun.star.ucb.CommandAbortedException cae) { + LogUtils.DEBUG("caught exception: " + cae.toString() + + " getting writable stream from " + url); + IOException newEx = new IOException(cae.getMessage()); + newEx.initCause(cae); + throw newEx; + } catch (com.sun.star.uno.Exception e) { + LogUtils.DEBUG("caught unknown exception: " + e.toString() + + " getting writable stream from " + url); + IOException newEx = new IOException(e.getMessage()); + newEx.initCause(e); + throw newEx; + } + + return os; + } + } + + private InputStream getUCBStream(String file, String path) throws IOException { + + InputStream is = null; + InputStream result = null; + + try { + if (path.endsWith(".jar")) { + is = m_jarStreamMap.get(path); + + if (is == null) { + is = getFileStreamFromUCB(path); + m_jarStreamMap.put(path, is); + } else { + try { + is.reset(); + } catch (IOException e) { + is.close(); + is = getFileStreamFromUCB(path); + m_jarStreamMap.put(path, is); + } + } + + result = getFileStreamFromJarStream(file, is); + } else { + String fileUrl = PathUtils.make_url(path, file); + result = getFileStreamFromUCB(fileUrl); + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ioe) { + LogUtils.DEBUG("Caught exception closing stream: " + + ioe.getMessage()); + } + } + } + + return result; + } + + private InputStream getFileStreamFromJarStream(String file, + InputStream is) throws + IOException { + + ZipInputStream zis = new ZipInputStream(is); + + while (zis.available() != 0) { + ZipEntry entry = zis.getNextEntry(); + + if (entry != null && entry.getName().equals(file)) { + return zis; + } + } + + return null; + } + + private InputStream getFileStreamFromUCB(String path) throws IOException { + + InputStream result = null; + XInputStream xInputStream = null; + + try { + LogUtils.DEBUG("Trying to read from " + path); + xInputStream = m_xSimpleFileAccess.openFileRead(path); + LogUtils.DEBUG("sfa appeared to read file "); + byte[][] inputBytes = new byte[1][]; + + int sz = m_xSimpleFileAccess.getSize(path); + + // TODO don't depend on result of available() or size() + // just read stream 'till complete + if (sz == 0 && xInputStream.available() > 0) { + sz = xInputStream.available(); + } + + LogUtils.DEBUG("size of file " + path + " is " + sz); + LogUtils.DEBUG("available = " + xInputStream.available()); + inputBytes[0] = new byte[sz]; + + int ln = xInputStream.readBytes(inputBytes, sz); + + if (ln != sz) { + throw new IOException( + "Failed to read " + sz + " bytes from XInputStream"); + } + + result = new ByteArrayInputStream(inputBytes[0]); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } catch (com.sun.star.uno.Exception ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } finally { + if (xInputStream != null) { + try { + xInputStream.closeInput(); + } catch (Exception e2) { + LogUtils.DEBUG("Error closing XInputStream: " + + e2.getMessage()); + } + } + } + + return result; + } +} diff --git a/scripting/java/com/sun/star/script/framework/io/XInputStreamImpl.java b/scripting/java/com/sun/star/script/framework/io/XInputStreamImpl.java new file mode 100644 index 000000000..8320b6227 --- /dev/null +++ b/scripting/java/com/sun/star/script/framework/io/XInputStreamImpl.java @@ -0,0 +1,109 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF 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 . + */ + +package com.sun.star.script.framework.io; + +import com.sun.star.io.XInputStream; + +import java.io.IOException; +import java.io.InputStream; + +public class XInputStreamImpl implements XInputStream { + + private final InputStream is; + + public XInputStreamImpl(InputStream is) { + this.is = is; + } + + public int readBytes(/*OUT*/byte[][] aData, /*IN*/int nBytesToRead) throws + com.sun.star.io.NotConnectedException, + com.sun.star.io.BufferSizeExceededException, com.sun.star.io.IOException { + + aData[ 0 ] = new byte[ nBytesToRead ]; + int totalBytesRead = 0; + + try { + int bytesRead; + + while ((bytesRead = is.read(aData[ 0 ], totalBytesRead, nBytesToRead)) > 0 + && (totalBytesRead < nBytesToRead)) { + totalBytesRead += bytesRead; + nBytesToRead -= bytesRead; + } + } catch (IOException e) { + throw new com.sun.star.io.IOException(e); + } catch (IndexOutOfBoundsException aie) { + throw new com.sun.star.io.BufferSizeExceededException(aie); + } + + return totalBytesRead; + } + + public int readSomeBytes(/*OUT*/byte[][] aData, /*IN*/int nMaxBytesToRead) + throws com.sun.star.io.NotConnectedException, + com.sun.star.io.BufferSizeExceededException, com.sun.star.io.IOException { + + int bytesToRead = nMaxBytesToRead; + int availableBytes = available(); + + if (availableBytes < nMaxBytesToRead) { + bytesToRead = availableBytes; + } + + int read = readBytes(aData, bytesToRead); + return read; + } + + public void skipBytes(/*IN*/int nBytesToSkip) throws + com.sun.star.io.NotConnectedException, + com.sun.star.io.BufferSizeExceededException, com.sun.star.io.IOException { + + try { + do { + nBytesToSkip -= is.skip(nBytesToSkip); + } while (nBytesToSkip > 0); + } catch (IOException e) { + throw new com.sun.star.io.IOException(e); + } + } + + public int available() throws + com.sun.star.io.NotConnectedException, com.sun.star.io.IOException { + + int bytesAvail = 0; + + try { + bytesAvail = is.available(); + } catch (IOException e) { + throw new com.sun.star.io.IOException(e); + } + + return bytesAvail; + } + + public void closeInput() throws + com.sun.star.io.NotConnectedException, com.sun.star.io.IOException { + + try { + is.close(); + } catch (IOException e) { + throw new com.sun.star.io.IOException(e); + } + } +}
\ No newline at end of file diff --git a/scripting/java/com/sun/star/script/framework/io/XInputStreamWrapper.java b/scripting/java/com/sun/star/script/framework/io/XInputStreamWrapper.java new file mode 100644 index 000000000..654f0e24c --- /dev/null +++ b/scripting/java/com/sun/star/script/framework/io/XInputStreamWrapper.java @@ -0,0 +1,100 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF 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 . + */ +package com.sun.star.script.framework.io; + +import com.sun.star.io.XInputStream; + +import java.io.IOException; +import java.io.InputStream; + +public class XInputStreamWrapper extends InputStream { + + private final XInputStream m_xInputStream; + + public XInputStreamWrapper(XInputStream xInputStream) { + m_xInputStream = xInputStream; + } + + @Override + public int read() throws java.io.IOException { + byte[][] byteRet = new byte[1][0]; + long numRead; + + try { + numRead = m_xInputStream.readBytes(byteRet, 1); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + + if (numRead != 1) { + return -1; + } + + return byteRet[0][0]; + } + + @Override + public int read(byte[] b) throws java.io.IOException { + byte[][] byteRet = new byte[1][]; + byteRet[0] = b; + + try { + return m_xInputStream.readBytes(byteRet, b.length); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + @Override + public long skip(long n) throws java.io.IOException { + try { + m_xInputStream.skipBytes((int)n); + return n; + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + @Override + public int available() throws java.io.IOException { + try { + return m_xInputStream.available(); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + @Override + public void close() throws java.io.IOException { + try { + m_xInputStream.closeInput(); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } +}
\ No newline at end of file diff --git a/scripting/java/com/sun/star/script/framework/io/XOutputStreamWrapper.java b/scripting/java/com/sun/star/script/framework/io/XOutputStreamWrapper.java new file mode 100644 index 000000000..16357fd17 --- /dev/null +++ b/scripting/java/com/sun/star/script/framework/io/XOutputStreamWrapper.java @@ -0,0 +1,113 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF 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 . + */ +package com.sun.star.script.framework.io; + +import com.sun.star.io.XOutputStream; + +import java.io.IOException; +import java.io.OutputStream; + +public class XOutputStreamWrapper extends OutputStream { + + private final XOutputStream m_xOutputStream; + + public XOutputStreamWrapper(XOutputStream xOs) { + this.m_xOutputStream = xOs; + } + + @Override + public void write(int b) throws java.io.IOException { + if (m_xOutputStream == null) { + throw new java.io.IOException("Stream is null"); + } + + byte[] bytes = new byte[1]; + bytes[0] = (byte) b; + + try { + m_xOutputStream.writeBytes(bytes); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + @Override + public void write(byte[] b) throws java.io.IOException { + + if (m_xOutputStream == null) { + throw new java.io.IOException("Stream is null"); + } + + try { + m_xOutputStream.writeBytes(b); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + @Override + public void write(byte[] b, int off, int len) throws java.io.IOException { + if (m_xOutputStream == null) { + throw new java.io.IOException("Stream is null"); + } + + byte[] bytes = new byte[len]; + System.arraycopy(b, off, bytes, 0, len); + + try { + m_xOutputStream.writeBytes(bytes); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + @Override + public void flush() throws java.io.IOException { + if (m_xOutputStream == null) { + throw new java.io.IOException("Stream is null"); + } + + try { + m_xOutputStream.flush(); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + @Override + public void close() throws java.io.IOException { + if (m_xOutputStream == null) { + throw new java.io.IOException("Stream is null"); + } + + try { + m_xOutputStream.closeOutput(); + } catch (com.sun.star.io.IOException ex1) { + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } +}
\ No newline at end of file diff --git a/scripting/java/com/sun/star/script/framework/io/XStorageHelper.java b/scripting/java/com/sun/star/script/framework/io/XStorageHelper.java new file mode 100644 index 000000000..79c5f10bc --- /dev/null +++ b/scripting/java/com/sun/star/script/framework/io/XStorageHelper.java @@ -0,0 +1,249 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF 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 . + */ + +package com.sun.star.script.framework.io; + +import com.sun.star.beans.XPropertySet; + +import com.sun.star.container.XNameAccess; + +import com.sun.star.document.XDocumentSubStorageSupplier; + +import com.sun.star.embed.XStorage; +import com.sun.star.embed.XTransactedObject; + +import com.sun.star.frame.XModel; + +import com.sun.star.lang.EventObject; +import com.sun.star.lang.XComponent; +import com.sun.star.lang.XEventListener; + +import com.sun.star.script.framework.log.LogUtils; +import com.sun.star.script.framework.provider.PathUtils; + +import com.sun.star.uno.AnyConverter; +import com.sun.star.uno.UnoRuntime; +import com.sun.star.uno.XInterface; + +import java.io.IOException; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +public class XStorageHelper implements XEventListener { + + XStorage[] xStorages; + static Map<String, XModel> modelMap = new HashMap<String, XModel>(); + XModel xModel = null; + private static XStorageHelper listener = new XStorageHelper(); + + private XStorageHelper() {} + + public XStorageHelper(String path, int mode, + boolean create) throws IOException { + + String modelUrl = null; + int indexOfScriptsDir = path.lastIndexOf("Scripts"); + + if (indexOfScriptsDir > -1) { + modelUrl = path.substring(0, indexOfScriptsDir - 1); + path = path.substring(indexOfScriptsDir, path.length()); + } + + LogUtils.DEBUG("XStorageHelper ctor, path: " + path); + this.xModel = getModelForURL(modelUrl); + + try { + StringTokenizer tokens = new StringTokenizer(path, "/"); + + if (tokens.countTokens() == 0) { + throw new IOException("Invalid path"); + } + + XDocumentSubStorageSupplier xDocumentSubStorageSupplier = + UnoRuntime.queryInterface(XDocumentSubStorageSupplier.class, + xModel); + + xStorages = new XStorage[tokens.countTokens()]; + + LogUtils.DEBUG("XStorageHelper ctor, path chunks length: " + + xStorages.length); + + for (int i = 0; i < xStorages.length; i++) { + LogUtils.DEBUG("XStorageHelper, processing index " + i); + String name = tokens.nextToken(); + LogUtils.DEBUG("XStorageHelper, getting: " + name); + XStorage storage = null; + + if (i == 0) { + storage = xDocumentSubStorageSupplier.getDocumentSubStorage(name, mode); + + if (storage == null) { + LogUtils.DEBUG("** boo hoo Storage is null "); + } + + XPropertySet xProps = + UnoRuntime.queryInterface(XPropertySet.class, storage); + + if (xProps != null) { + + String mediaType = + AnyConverter.toString(xProps.getPropertyValue("MediaType")); + + LogUtils.DEBUG("***** media type is " + mediaType); + + if (!mediaType.equals("scripts")) { + xProps.setPropertyValue("MediaType", "scripts"); + } + } + } else { + + XNameAccess xNameAccess = + UnoRuntime.queryInterface(XNameAccess.class, xStorages[i - 1]); + + if (xNameAccess == null) { + disposeObject(); + throw new IOException("No name access " + name); + } else if (!xNameAccess.hasByName(name) + || !xStorages[i - 1].isStorageElement(name)) { + if (!create) { + disposeObject(); + throw new IOException("No subdir: " + name); + } else { + // attempt to create new storage + LogUtils.DEBUG("Attempt to create new storage for " + + name); + } + } + + storage = xStorages[i - 1].openStorageElement( + name, mode); + } + + if (storage == null) { + disposeObject(); + throw new IOException("storage not found: " + name); + } + + xStorages[i] = storage; + + } + } catch (com.sun.star.io.IOException ioe) { + disposeObject(); + } catch (com.sun.star.uno.Exception ex1) { + disposeObject(); + IOException ex2 = new IOException(); + ex2.initCause(ex1); + throw ex2; + } + } + + public synchronized static void addNewModel(XModel model) { + // TODO needs to cater for model for untitled document + modelMap.put(PathUtils.getOidForModel(model), model); + XComponent xComp = UnoRuntime.queryInterface(XComponent.class, model); + + if (xComp != null) { + try { + xComp.addEventListener(listener); + } catch (Exception e) { + // What TODO here ? + LogUtils.DEBUG(LogUtils.getTrace(e)); + } + } + } + + public void disposing(EventObject Source) { + XModel model = UnoRuntime.queryInterface(XModel.class, Source.Source); + + if (model != null) { + LogUtils.DEBUG(" Disposing doc " + model.getURL()); + modelMap.remove(PathUtils.getOidForModel(model)); + } + } + + public XStorage getStorage() { + return xStorages[ xStorages.length - 1 ]; + } + + public XModel getModel() { + return xModel; + } + + public void disposeObject() { + disposeObject(false); + } + + public void disposeObject(boolean shouldCommit) { + LogUtils.DEBUG("In disposeObject"); + + for (int i = xStorages.length - 1 ; i > -1; i--) { + LogUtils.DEBUG("In disposeObject disposing storage " + i); + + try { + XStorage xStorage = xStorages[i]; + + if (shouldCommit) { + commit(xStorage); + } + + disposeObject(xStorage); + LogUtils.DEBUG("In disposeObject disposed storage " + i); + } catch (Exception ignore) { + LogUtils.DEBUG("Exception disposing storage " + i); + } + + } + + } + + public static void disposeObject(XInterface xInterface) { + if (xInterface == null) { + return; + } + + XComponent xComponent = + UnoRuntime.queryInterface(XComponent.class, xInterface); + + if (xComponent == null) { + return; + } + + xComponent.dispose(); + } + + public static void commit(XInterface xInterface) { + + XTransactedObject xTrans = + UnoRuntime.queryInterface(XTransactedObject.class, xInterface); + + if (xTrans != null) { + try { + xTrans.commit(); + } catch (Exception e) { + LogUtils.DEBUG("Something went belly up exception: " + e); + } + } + } + + public XModel getModelForURL(String url) { + //TODO does not cater for untitled documents + return modelMap.get(url); + } +} |