git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@405300 13f79535-47bb-0310-9956-ffa450edef68master
@@ -410,6 +410,8 @@ Other changes: | |||||
* Handling of ' ', '#' in CLASSPATH and '#' in -lib (cannot use ' ' | * Handling of ' ', '#' in CLASSPATH and '#' in -lib (cannot use ' ' | ||||
in -lib on UNIX at the moment). Bugzilla Report 39295. | in -lib on UNIX at the moment). Bugzilla Report 39295. | ||||
* <scp> now optionally supports the sftp protocol. Bugzilla Report 39373. | |||||
Changes from Ant 1.6.4 to Ant 1.6.5 | Changes from Ant 1.6.4 to Ant 1.6.5 | ||||
=================================== | =================================== | ||||
@@ -1,5 +1,5 @@ | |||||
/* | /* | ||||
* Copyright 2003-2005 The Apache Software Foundation | |||||
* Copyright 2003-2006 The Apache Software Foundation | |||||
* | * | ||||
* Licensed under the Apache License, Version 2.0 (the "License"); | * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
* you may not use this file except in compliance with the License. | * you may not use this file except in compliance with the License. | ||||
@@ -21,6 +21,10 @@ import com.jcraft.jsch.Channel; | |||||
import com.jcraft.jsch.ChannelExec; | import com.jcraft.jsch.ChannelExec; | ||||
import com.jcraft.jsch.JSchException; | import com.jcraft.jsch.JSchException; | ||||
import com.jcraft.jsch.Session; | import com.jcraft.jsch.Session; | ||||
import com.jcraft.jsch.ChannelSftp; | |||||
import com.jcraft.jsch.SftpATTRS; | |||||
import com.jcraft.jsch.SftpException; | |||||
import com.jcraft.jsch.SftpProgressMonitor; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
@@ -74,6 +78,17 @@ public abstract class AbstractSshMessage { | |||||
return channel; | return channel; | ||||
} | } | ||||
/** | |||||
* Open an ssh sftp channel. | |||||
* @return the channel | |||||
* @throws JSchException on error | |||||
*/ | |||||
protected ChannelSftp openSftpChannel() throws JSchException { | |||||
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); | |||||
return channel; | |||||
} | |||||
/** | /** | ||||
* Send an ack. | * Send an ack. | ||||
* @param out the output stream to use | * @param out the output stream to use | ||||
@@ -213,4 +228,39 @@ public abstract class AbstractSshMessage { | |||||
return percent; | return percent; | ||||
} | } | ||||
private ProgressMonitor monitor = null; | |||||
protected SftpProgressMonitor getProgressMonitor(){ | |||||
if (monitor == null) { | |||||
monitor = new ProgressMonitor(); | |||||
} | |||||
return monitor; | |||||
} | |||||
private class ProgressMonitor implements SftpProgressMonitor { | |||||
private long initFileSize = 0; | |||||
private long totalLength = 0; | |||||
private int percentTransmitted = 0; | |||||
public void init(int op, String src, String dest, long max) { | |||||
initFileSize = max; | |||||
totalLength = 0; | |||||
percentTransmitted = 0; | |||||
} | |||||
public boolean count(long len) { | |||||
totalLength += len; | |||||
percentTransmitted = trackProgress(initFileSize, | |||||
totalLength, | |||||
percentTransmitted); | |||||
return true; | |||||
} | |||||
public void end() { | |||||
} | |||||
public long getTotalLength() { | |||||
return totalLength; | |||||
} | |||||
} | |||||
} | } |
@@ -1,5 +1,5 @@ | |||||
/* | /* | ||||
* Copyright 2003-2005 The Apache Software Foundation | |||||
* Copyright 2003-2006 The Apache Software Foundation | |||||
* | * | ||||
* Licensed under the Apache License, Version 2.0 (the "License"); | * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
* you may not use this file except in compliance with the License. | * you may not use this file except in compliance with the License. | ||||
@@ -44,6 +44,7 @@ public class Scp extends SSHBase { | |||||
private String toUri; | private String toUri; | ||||
private List fileSets = null; | private List fileSets = null; | ||||
private boolean isFromRemote, isToRemote; | private boolean isFromRemote, isToRemote; | ||||
private boolean isSftp = false; | |||||
/** | /** | ||||
* Sets the file to be transferred. This can either be a remote | * Sets the file to be transferred. This can either be a remote | ||||
@@ -141,6 +142,15 @@ public class Scp extends SSHBase { | |||||
this.isToRemote = true; | this.isToRemote = true; | ||||
} | } | ||||
/** | |||||
* Setting this to true to use sftp protocol. | |||||
* | |||||
* @param yesOrNo if true sftp protocol will be used. | |||||
*/ | |||||
public void setSftp(boolean yesOrNo) { | |||||
isSftp = yesOrNo; | |||||
} | |||||
/** | /** | ||||
* Adds a FileSet tranfer to remote host. NOTE: Either | * Adds a FileSet tranfer to remote host. NOTE: Either | ||||
* addFileSet() or setFile() are required. But, not both. | * addFileSet() or setFile() are required. But, not both. | ||||
@@ -213,10 +223,18 @@ public class Scp extends SSHBase { | |||||
Session session = null; | Session session = null; | ||||
try { | try { | ||||
session = openSession(); | session = openSession(); | ||||
ScpFromMessage message = | |||||
new ScpFromMessage(getVerbose(), session, file, | |||||
getProject().resolveFile(toPath), | |||||
fromSshUri.endsWith("*")); | |||||
ScpFromMessage message = null; | |||||
if (!isSftp){ | |||||
message = | |||||
new ScpFromMessage(getVerbose(), session, file, | |||||
getProject().resolveFile(toPath), | |||||
fromSshUri.endsWith("*")); | |||||
} else{ | |||||
message = | |||||
new ScpFromMessageBySftp(getVerbose(), session, file, | |||||
getProject().resolveFile(toPath), | |||||
fromSshUri.endsWith("*")); | |||||
} | |||||
log("Receiving file: " + file); | log("Receiving file: " + file); | ||||
message.setLogListener(this); | message.setLogListener(this); | ||||
message.execute(); | message.execute(); | ||||
@@ -243,8 +261,14 @@ public class Scp extends SSHBase { | |||||
} | } | ||||
if (!list.isEmpty()) { | if (!list.isEmpty()) { | ||||
session = openSession(); | session = openSession(); | ||||
ScpToMessage message = new ScpToMessage(getVerbose(), session, | |||||
list, file); | |||||
ScpToMessage message = null; | |||||
if (!isSftp){ | |||||
message = new ScpToMessage(getVerbose(), session, | |||||
list, file); | |||||
} else{ | |||||
message = new ScpToMessageBySftp(getVerbose(), session, | |||||
list, file); | |||||
} | |||||
message.setLogListener(this); | message.setLogListener(this); | ||||
message.execute(); | message.execute(); | ||||
} | } | ||||
@@ -262,9 +286,17 @@ public class Scp extends SSHBase { | |||||
Session session = null; | Session session = null; | ||||
try { | try { | ||||
session = openSession(); | session = openSession(); | ||||
ScpToMessage message = | |||||
new ScpToMessage(getVerbose(), session, | |||||
getProject().resolveFile(fromPath), file); | |||||
ScpToMessage message = null; | |||||
if (!isSftp){ | |||||
message = | |||||
new ScpToMessage(getVerbose(), session, | |||||
getProject().resolveFile(fromPath), file); | |||||
} else{ | |||||
message = | |||||
new ScpToMessageBySftp(getVerbose(), session, | |||||
getProject().resolveFile(fromPath), | |||||
file); | |||||
} | |||||
message.setLogListener(this); | message.setLogListener(this); | ||||
message.execute(); | message.execute(); | ||||
} finally { | } finally { | ||||
@@ -1,5 +1,5 @@ | |||||
/* | /* | ||||
* Copyright 2003-2005 The Apache Software Foundation | |||||
* Copyright 2003-2006 The Apache Software Foundation | |||||
* | * | ||||
* Licensed under the Apache License, Version 2.0 (the "License"); | * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
* you may not use this file except in compliance with the License. | * you may not use this file except in compliance with the License. | ||||
@@ -40,6 +40,24 @@ public class ScpFromMessage extends AbstractSshMessage { | |||||
private File localFile; | private File localFile; | ||||
private boolean isRecursive = false; | private boolean isRecursive = false; | ||||
/** | |||||
* Constructor for ScpFromMessage | |||||
* @param session the ssh session to use | |||||
*/ | |||||
public ScpFromMessage(Session session) { | |||||
super(session); | |||||
} | |||||
/** | |||||
* Constructor for ScpFromMessage | |||||
* @param verbose if true do verbose logging | |||||
* @param session the ssh session to use | |||||
* @since Ant 1.6.2 | |||||
*/ | |||||
public ScpFromMessage(boolean verbose, Session session) { | |||||
super(verbose, session); | |||||
} | |||||
/** | /** | ||||
* Constructor for ScpFromMessage. | * Constructor for ScpFromMessage. | ||||
* @param verbose if true log extra information | * @param verbose if true log extra information | ||||
@@ -0,0 +1,173 @@ | |||||
/* | |||||
* Copyright 2006 The Apache Software Foundation | |||||
* | |||||
* 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.apache.tools.ant.taskdefs.optional.ssh; | |||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import java.io.EOFException; | |||||
import java.io.InputStream; | |||||
import java.io.OutputStream; | |||||
import java.io.FileOutputStream; | |||||
import java.io.ByteArrayOutputStream; | |||||
import com.jcraft.jsch.JSchException; | |||||
import com.jcraft.jsch.Session; | |||||
import com.jcraft.jsch.Channel; | |||||
import com.jcraft.jsch.ChannelSftp; | |||||
import com.jcraft.jsch.SftpException; | |||||
import com.jcraft.jsch.SftpATTRS; | |||||
import com.jcraft.jsch.SftpProgressMonitor; | |||||
/** | |||||
* A helper object representing an scp download. | |||||
*/ | |||||
public class ScpFromMessageBySftp extends ScpFromMessage { | |||||
private String remoteFile; | |||||
private File localFile; | |||||
private boolean isRecursive = false; | |||||
private boolean verbose = false; | |||||
/** | |||||
* Constructor for ScpFromMessageBySftp. | |||||
* @param verbose if true log extra information | |||||
* @param session the Scp session to use | |||||
* @param aRemoteFile the remote file name | |||||
* @param aLocalFile the local file | |||||
* @param recursive if true use recursion | |||||
* @since Ant 1.7 | |||||
*/ | |||||
public ScpFromMessageBySftp(boolean verbose, | |||||
Session session, | |||||
String aRemoteFile, | |||||
File aLocalFile, | |||||
boolean recursive) { | |||||
super(verbose, session); | |||||
this.verbose = verbose; | |||||
this.remoteFile = aRemoteFile; | |||||
this.localFile = aLocalFile; | |||||
this.isRecursive = recursive; | |||||
} | |||||
/** | |||||
* Constructor for ScpFromMessageBySftp. | |||||
* @param session the Scp session to use | |||||
* @param aRemoteFile the remote file name | |||||
* @param aLocalFile the local file | |||||
* @param recursive if true use recursion | |||||
*/ | |||||
public ScpFromMessageBySftp(Session session, | |||||
String aRemoteFile, | |||||
File aLocalFile, | |||||
boolean recursive) { | |||||
this(false, session, aRemoteFile, aLocalFile, recursive); | |||||
} | |||||
/** | |||||
* Carry out the transfer. | |||||
* @throws IOException on i/o errors | |||||
* @throws JSchException on errors detected by scp | |||||
*/ | |||||
public void execute() throws IOException, JSchException { | |||||
ChannelSftp channel = openSftpChannel(); | |||||
try { | |||||
channel.connect(); | |||||
try { | |||||
SftpATTRS attrs = channel.stat(remoteFile); | |||||
if (attrs.isDir() && !remoteFile.endsWith("/")) { | |||||
remoteFile=remoteFile+"/"; | |||||
} | |||||
} catch(SftpException ee) { | |||||
} | |||||
getDir(channel, remoteFile, localFile); | |||||
} catch(SftpException e) { | |||||
throw new JSchException(e.toString()); | |||||
} finally { | |||||
if (channel != null) { | |||||
channel.disconnect(); | |||||
} | |||||
} | |||||
log("done\n"); | |||||
} | |||||
private void getDir(ChannelSftp channel, | |||||
String remoteFile, | |||||
File localFile) throws IOException, SftpException { | |||||
String pwd=remoteFile; | |||||
if (remoteFile.lastIndexOf('/')!=-1) { | |||||
if (remoteFile.length()>1) { | |||||
pwd=remoteFile.substring(0, remoteFile.lastIndexOf('/')); | |||||
} | |||||
} | |||||
channel.cd(pwd); | |||||
if (!localFile.exists()) { | |||||
localFile.mkdirs(); | |||||
} | |||||
java.util.Vector files = channel.ls(remoteFile); | |||||
for(int i = 0; i < files.size(); i++){ | |||||
ChannelSftp.LsEntry le = (ChannelSftp.LsEntry) files.elementAt(i); | |||||
String name = le.getFilename(); | |||||
if (le.getAttrs().isDir()) { | |||||
if (name.equals(".") || name.equals("..")) { | |||||
continue; | |||||
} | |||||
getDir(channel, | |||||
channel.pwd() + "/" + name + "/", | |||||
new File(localFile, le.getFilename())); | |||||
} else{ | |||||
getFile(channel, le, localFile); | |||||
} | |||||
} | |||||
channel.cd(".."); | |||||
} | |||||
private void getFile(ChannelSftp channel, | |||||
ChannelSftp.LsEntry le, | |||||
File localFile) throws IOException, SftpException { | |||||
String remoteFile = le.getFilename(); | |||||
if (!localFile.exists()) { | |||||
String path = localFile.getAbsolutePath(); | |||||
int i = 0; | |||||
if ((i = path.lastIndexOf(File.pathSeparator)) != -1) { | |||||
if (path.length()>File.pathSeparator.length()) { | |||||
new File(path.substring(0, i)).mkdirs(); | |||||
} | |||||
} | |||||
} | |||||
if (localFile.isDirectory()) { | |||||
localFile=new File(localFile, remoteFile); | |||||
} | |||||
long startTime = System.currentTimeMillis(); | |||||
long totalLength = le.getAttrs().getSize(); | |||||
SftpProgressMonitor monitor = null; | |||||
boolean trackProgress = getVerbose() && totalLength > 102400; | |||||
if (trackProgress){ | |||||
monitor = getProgressMonitor(); | |||||
} | |||||
try{ | |||||
log("Receiving: " + remoteFile + " : " + le.getAttrs().getSize()); | |||||
channel.get(remoteFile, localFile.getAbsolutePath(), monitor); | |||||
} finally{ | |||||
long endTime = System.currentTimeMillis(); | |||||
logStats(startTime, endTime, (int)totalLength); | |||||
} | |||||
} | |||||
} |
@@ -39,6 +39,24 @@ public class ScpToMessage extends AbstractSshMessage { | |||||
private String remotePath; | private String remotePath; | ||||
private List directoryList; | private List directoryList; | ||||
/** | |||||
* Constructor for ScpToMessage | |||||
* @param session the ssh session to use | |||||
*/ | |||||
public ScpToMessage(Session session) { | |||||
super(session); | |||||
} | |||||
/** | |||||
* Constructor for ScpToMessage | |||||
* @param verbose if true do verbose logging | |||||
* @param session the ssh session to use | |||||
* @since Ant 1.7 | |||||
*/ | |||||
public ScpToMessage(boolean verbose, Session session) { | |||||
super(verbose, session); | |||||
} | |||||
/** | /** | ||||
* Constructor for a local file to remote. | * Constructor for a local file to remote. | ||||
* @param verbose if true do verbose logging | * @param verbose if true do verbose logging | ||||
@@ -0,0 +1,240 @@ | |||||
/* | |||||
* Copyright 2006 The Apache Software Foundation | |||||
* | |||||
* 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.apache.tools.ant.taskdefs.optional.ssh; | |||||
import com.jcraft.jsch.Channel; | |||||
import com.jcraft.jsch.Session; | |||||
import com.jcraft.jsch.ChannelSftp; | |||||
import com.jcraft.jsch.JSchException; | |||||
import com.jcraft.jsch.SftpException; | |||||
import com.jcraft.jsch.SftpProgressMonitor; | |||||
import com.jcraft.jsch.SftpATTRS; | |||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import java.io.InputStream; | |||||
import java.io.FileInputStream; | |||||
import java.io.OutputStream; | |||||
import java.util.List; | |||||
import java.util.Iterator; | |||||
public class ScpToMessageBySftp extends ScpToMessage/*AbstractSshMessage*/ { | |||||
private File localFile; | |||||
private String remotePath; | |||||
private List directoryList; | |||||
/** | |||||
* Constructor for a local file to remote. | |||||
* @param verbose if true do verbose logging | |||||
* @param session the scp session to use | |||||
* @param aLocalFile the local file | |||||
* @param aRemotePath the remote path | |||||
* @since Ant 1.7 | |||||
*/ | |||||
public ScpToMessageBySftp(boolean verbose, | |||||
Session session, | |||||
File aLocalFile, | |||||
String aRemotePath) { | |||||
this(verbose, session, aRemotePath); | |||||
this.localFile = aLocalFile; | |||||
} | |||||
/** | |||||
* Constructor for a local directories to remote. | |||||
* @param verbose if true do verbose logging | |||||
* @param session the scp session to use | |||||
* @param aDirectoryList a list of directories | |||||
* @param aRemotePath the remote path | |||||
* @since Ant 1.7 | |||||
*/ | |||||
public ScpToMessageBySftp(boolean verbose, | |||||
Session session, | |||||
List aDirectoryList, | |||||
String aRemotePath) { | |||||
this(verbose, session, aRemotePath); | |||||
this.directoryList = aDirectoryList; | |||||
} | |||||
/** | |||||
* Constructor for ScpToMessage. | |||||
* @param verbose if true do verbose logging | |||||
* @param session the scp session to use | |||||
* @param aRemotePath the remote path | |||||
* @since Ant 1.6.2 | |||||
*/ | |||||
private ScpToMessageBySftp(boolean verbose, | |||||
Session session, | |||||
String aRemotePath) { | |||||
super(verbose, session); | |||||
this.remotePath = aRemotePath; | |||||
} | |||||
/** | |||||
* Constructor for ScpToMessage. | |||||
* @param session the scp session to use | |||||
* @param aLocalFile the local file | |||||
* @param aRemotePath the remote path | |||||
*/ | |||||
public ScpToMessageBySftp(Session session, | |||||
File aLocalFile, | |||||
String aRemotePath) { | |||||
this(false, session, aLocalFile, aRemotePath); | |||||
} | |||||
/** | |||||
* Constructor for ScpToMessage. | |||||
* @param session the scp session to use | |||||
* @param aDirectoryList a list of directories | |||||
* @param aRemotePath the remote path | |||||
*/ | |||||
public ScpToMessageBySftp(Session session, | |||||
List aDirectoryList, | |||||
String aRemotePath) { | |||||
this(false, session, aDirectoryList, aRemotePath); | |||||
} | |||||
/** | |||||
* Carry out the transfer. | |||||
* @throws IOException on i/o errors | |||||
* @throws JSchException on errors detected by scp | |||||
*/ | |||||
public void execute() throws IOException, JSchException { | |||||
if (directoryList != null) { | |||||
doMultipleTransfer(); | |||||
} | |||||
if (localFile != null) { | |||||
doSingleTransfer(); | |||||
} | |||||
log("done.\n"); | |||||
} | |||||
private void doSingleTransfer() throws IOException, JSchException { | |||||
ChannelSftp channel = openSftpChannel(); | |||||
try { | |||||
channel.connect(); | |||||
try{ | |||||
sendFileToRemote(channel, localFile, remotePath); | |||||
} | |||||
catch(SftpException e){ | |||||
throw new JSchException(e.toString()); | |||||
} | |||||
} finally { | |||||
if (channel != null) { | |||||
channel.disconnect(); | |||||
} | |||||
} | |||||
} | |||||
private void doMultipleTransfer() throws IOException, JSchException { | |||||
ChannelSftp channel = openSftpChannel(); | |||||
try { | |||||
channel.connect(); | |||||
try{ | |||||
channel.cd(remotePath); | |||||
for (Iterator i = directoryList.iterator(); i.hasNext();) { | |||||
Directory current = (Directory) i.next(); | |||||
sendDirectory(channel, current); | |||||
} | |||||
} | |||||
catch(SftpException e){ | |||||
throw new JSchException(e.toString()); | |||||
} | |||||
} finally { | |||||
if (channel != null) { | |||||
channel.disconnect(); | |||||
} | |||||
} | |||||
} | |||||
private void sendDirectory(ChannelSftp channel, | |||||
Directory current) | |||||
throws IOException, SftpException { | |||||
for (Iterator fileIt = current.filesIterator(); fileIt.hasNext();) { | |||||
sendFileToRemote(channel, (File) fileIt.next(), null); | |||||
} | |||||
for (Iterator dirIt = current.directoryIterator(); dirIt.hasNext();) { | |||||
Directory dir = (Directory) dirIt.next(); | |||||
sendDirectoryToRemote(channel, dir); | |||||
} | |||||
} | |||||
private void sendDirectoryToRemote(ChannelSftp channel, | |||||
Directory directory) | |||||
throws IOException, SftpException { | |||||
String dir=directory.getDirectory().getName(); | |||||
try{ | |||||
channel.stat(dir); | |||||
} | |||||
catch(SftpException e){ | |||||
// dir does not exist. | |||||
if (e.id==ChannelSftp.SSH_FX_NO_SUCH_FILE) { | |||||
channel.mkdir(dir); | |||||
} | |||||
} | |||||
channel.cd(dir); | |||||
sendDirectory(channel, directory); | |||||
channel.cd(".."); | |||||
} | |||||
private void sendFileToRemote(ChannelSftp channel, | |||||
File localFile, | |||||
String remotePath) | |||||
throws IOException, SftpException { | |||||
long filesize = localFile.length(); | |||||
if (remotePath==null) { | |||||
remotePath=localFile.getName(); | |||||
} | |||||
long startTime = System.currentTimeMillis(); | |||||
long totalLength = filesize; | |||||
// only track progress for files larger than 100kb in verbose mode | |||||
boolean trackProgress = getVerbose() && filesize > 102400; | |||||
SftpProgressMonitor monitor = null; | |||||
if (trackProgress){ | |||||
monitor = getProgressMonitor(); | |||||
} | |||||
try{ | |||||
if (this.getVerbose()) { | |||||
log("Sending: " + localFile.getName() + " : " + filesize); | |||||
} | |||||
channel.put(localFile.getAbsolutePath(), remotePath, monitor); | |||||
} | |||||
finally { | |||||
if (this.getVerbose()) { | |||||
long endTime = System.currentTimeMillis(); | |||||
logStats(startTime, endTime, (int) totalLength); | |||||
} | |||||
} | |||||
} | |||||
public File getLocalFile() { | |||||
return localFile; | |||||
} | |||||
public String getRemotePath() { | |||||
return remotePath; | |||||
} | |||||
} |