1
\$\begingroup\$

This is a follow-up question for Android APP FTP host profile class implementation and Android APP connect to FTP server in Java. I am attempting to perform the upload operation to specified FTP server in Android APP. With referring to the answer of FtpClient storeFile always return False and List of FTP server return codes on wikipedia, the returned reply code is translated to string message.

The experimental implementation

  • FtpConnection class implementation:

    package com.example.ftpreplycodeclass;
    
    import android.util.Log;
    
    import org.apache.commons.net.ftp.FTP;
    import org.apache.commons.net.ftp.FTPClient;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.net.SocketException;
    import java.net.UnknownHostException;
    
    public final class FtpConnection {
    
        private final FtpHostProfiles ftpHostProfiles = new FtpHostProfiles();
    
        public FtpConnection(FtpHostProfiles input) {
            this.ftpHostProfiles.addProfiles(input);
        }
    
        public boolean uploadFile(String profileName, String fullFilename) {
            File targetFile = new File(fullFilename);
            if (!targetFile.exists()) {
                Log.e("FTPconnection_uploadFile", "File " + fullFilename + " isn't existed!");
                return false;
            }
    
            //  Reference: https://stackoverflow.com/a/8761268/6667035
            FTPClient ftp = new FTPClient();
            try {
                FtpHostProfile profile = ftpHostProfiles.getProfile(profileName);
    
                //  Reference: https://stackoverflow.com/a/55950845/6667035
                //  The argument of `FTPClient.connect` method is hostname, not URL.
                ftp.connect(profile.getHostname(), profile.getPort());
                boolean status = ftp.login(profile.getUsername(), profile.getPassword());
                if (status)
                {
                    ftp.enterLocalPassiveMode();
                    ftp.setFileType(FTP.BINARY_FILE_TYPE);
                    ftp.sendCommand("OPTS UTF8 ON");
                }
                System.out.println("status : " + ftp.getStatus());
                Log.d("FTPTask", ftp.getStatus());
                FileInputStream in = new FileInputStream(fullFilename);
                String filename = new File(fullFilename).getName();
                boolean result = ftp.storeFile(filename, in);
                var detailedInfo = ftp.getReplyCode();
                Log.d("FTP Reply Code", String.valueOf(detailedInfo));
                Log.d("FTP Reply Message", FtpReplyCode.getReplyMessage(detailedInfo));
                in.close();
                ftp.logout();
                ftp.disconnect();
                return result;
            } catch (UnknownHostException ex) {
                ex.printStackTrace();
            } catch (SocketException en) {
                // TODO Auto-generated catch block
                en.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return false;
        }
    
        public FTPClient connectftp(String profileName) {
            //  Reference: https://stackoverflow.com/a/8761268/6667035
            FTPClient ftp = new FTPClient();
            try {
                FtpHostProfile profile = ftpHostProfiles.getProfile(profileName);
    
                //  Reference: https://stackoverflow.com/a/55950845/6667035
                //  The argument of `FtpClient.connect` method is hostname, not URL.
                ftp.connect(profile.getHostname(), profile.getPort());
                boolean loggedIn = ftp.login(profile.getUsername(), profile.getPassword());
                if (loggedIn) {
                    ftp.enterLocalPassiveMode();
                    ftp.setFileType(FTP.BINARY_FILE_TYPE);
                    ftp.sendCommand("OPTS UTF8 ON");
                }
                System.out.println("status : " + ftp.getStatus());
            } catch (UnknownHostException ex) {
                ex.printStackTrace();
            } catch (SocketException en) {
                en.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return ftp;
        }
    
        private static class FtpReplyCode {
    
            private static String getReplyMessage(int input) {
                if (input == 100) {
                    return "The requested action is being initiated, expect another reply before proceeding with a new command.";
                }
                if (input == 110) {
                    return "Restart marker replay.";
                }
                if (input == 120) {
                    return "Service ready in nnn minutes.";
                }
                if (input == 125) {
                    return "Data connection already open; transfer starting.";
                }
                if (input == 150) {
                    return "File status okay; about to open data connection.";
                }
                if (input == 200) {
                    return "The requested action has been successfully completed.";
                }
                if (input == 202) {
                    return "Command not implemented, superfluous at this site.";
                }
                if (input == 211) {
                    return "System status, or system help reply.";
                }
                if (input == 212) {
                    return "Directory status.";
                }
                if (input == 213) {
                    return "File status.";
                }
                if (input == 214) {
                    return "Help message. Explains how to use the server or the meaning of a particular non-standard command. This reply is useful only to the human user.";
                }
                if (input == 215) {
                    return "NAME system type. Where NAME is an official system name from the registry kept by IANA.";
                }
                if (input == 220) {
                    return "Service ready for new user.";
                }
                if (input == 221) {
                    return "Service closing control connection.";
                }
                if (input == 225) {
                    return "Data connection open; no transfer in progress.";
                }
                if (input == 226) {
                    return "Closing data connection. Requested file action successful (for example, file transfer or file abort).";
                }
                if (input == 227) {
                    return "Entering Passive Mode (h1,h2,h3,h4,p1,p2).";
                }
                if (input == 228) {
                    return "Entering Long Passive Mode (long address, port).";
                }
                if (input == 229) {
                    return "Entering Extended Passive Mode (|||port|).";
                }
                if (input == 230) {
                    return "User logged in, proceed. Logged out if appropriate.";
                }
                if (input == 231) {
                    return "User logged out; service terminated.";
                }
                if (input == 232) {
                    return "Logout command noted, will complete when transfer done.";
                }
                if (input == 234) {
                    return "Specifies that the server accepts the authentication mechanism specified by the client, and the exchange of security data is complete. A higher level nonstandard code created by Microsoft.";
                }
                if (input == 250) {
                    return "Requested file action okay, completed.";
                }
                if (input == 257) {
                    return "\"PATHNAME\" created";
                }
                if (input == 300) {
                    return "The command has been accepted, but the requested action is on hold, pending receipt of further information.";
                }
                if (input == 331) {
                    return "User name okay, need password.";
                }
                if (input == 332) {
                    return "Need account for login.";
                }
                if (input == 350) {
                    return "Requested file action pending further information";
                }
                if (input == 400) {
                    return "The command was not accepted and the requested action did not take place, but the error condition is temporary and the action may be requested again.";
                }
                if (input == 421) {
                    return "Service not available, closing control connection. This may be a reply to any command if the service knows it must shut down.";
                }
                if (input == 425) {
                    return "Can\'t open data connection.";
                }
                if (input == 426) {
                    return "Connection closed; transfer aborted.";
                }
                if (input == 430) {
                    return "Invalid username or password";
                }
                if (input == 434) {
                    return "Requested host unavailable.";
                }
                if (input == 450) {
                    return "Requested file action not taken.";
                }
                if (input == 451) {
                    return "Requested action aborted. Local error in processing.";
                }
                if (input == 452) {
                    return "Requested action not taken. Insufficient storage space in system. File unavailable (e.g., file busy).";
                }
                if (input == 500) {
                    return "Syntax error, command unrecognized and the requested action did not take place. This may include errors such as command line too long.";
                }
                if (input == 501) {
                    return "Syntax error in parameters or arguments.";
                }
                if (input == 502) {
                    return "Command not implemented.";
                }
                if (input == 503) {
                    return "Bad sequence of commands.";
                }
                if (input == 504) {
                    return "Command not implemented for that parameter.";
                }
                if (input == 530) {
                    return "Not logged in.";
                }
                if (input == 532) {
                    return "Need account for storing files.";
                }
                if (input == 534) {
                    return "Could Not Connect to Server - Policy Requires SSL";
                }
                if (input == 550) {
                    return "Requested action not taken. File unavailable (e.g., file not found, no access).";
                }
                if (input == 551) {
                    return "Requested action aborted. Page type unknown.";
                }
                if (input == 552) {
                    return "Requested file action aborted. Exceeded storage allocation (for current directory or dataset).";
                }
                if (input == 553) {
                    return "Requested action not taken. File name not allowed.";
                }
                if (input == 600) {
                    return "Replies regarding confidentiality and integrity";
                }
                if (input == 631) {
                    return "Integrity protected reply.";
                }
                if (input == 632) {
                    return "Confidentiality and integrity protected reply.";
                }
                if (input == 633) {
                    return "Confidentiality protected reply.";
                }
                if (input == 10000) {
                    return "Common Winsock Error Codes";
                }
                if (input == 10054) {
                    return "Connection reset by peer. The connection was forcibly closed by the remote host.";
                }
                if (input == 10060) {
                    return "Cannot connect to remote server.";
                }
                if (input == 10061) {
                    return "Cannot connect to remote server. The connection is actively refused by the server.";
                }
                if (input == 10066) {
                    return "Directory not empty.";
                }
                if (input == 10068) {
                    return "Too many users, server is full.";
                }
                return "";
            }
        }
    }
    

Full Testing Code

  • MainActivity.java implementation:

    package com.example.ftpreplycodeclass;
    
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.app.ActivityCompat;
    import androidx.core.content.ContextCompat;
    
    import android.Manifest;
    import android.content.Context;
    import android.content.pm.PackageManager;
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.util.Log;
    import android.widget.Toast;
    
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.TimeZone;
    
    public class MainActivity extends AppCompatActivity {
    
        private static final int READ_EXTERNAL_STORAGE_CODE = 1;
        private static final int WRITE_EXTERNAL_STORAGE_CODE = 2;
        private static final int ACCESS_NETWORK_STATE_CODE = 3;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE_CODE);
            checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE_CODE);
            checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, ACCESS_NETWORK_STATE_CODE);
    
            User currentUser = new User(
                    "Mike",
                    "M12345678",
                    "1990/10/13",
                    "(555) 555-1234",
                    "123456@test.com",
                    "password");
    
            //  Using GMT (UTC±00:00) time
            var gmtTimeZone = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
            gmtTimeZone.setTimeZone(TimeZone.getTimeZone("GMT"));
            String currentTime = gmtTimeZone.format(Calendar.getInstance().getTime());
            String newUserRegistrationFilename = currentTime + "_NewUser.ser";
            boolean isSaveSuccessfully = Serialization.Save(this, newUserRegistrationFilename, currentUser);
            if (isSaveSuccessfully) {
                showToast("Save current user information successfully", Toast.LENGTH_SHORT);
                Log.d("Save operation", "Save current user information successfully");
            }
            showToast(Serialization.Load(this, newUserRegistrationFilename).getFullName(), Toast.LENGTH_SHORT);
    
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            new FtpTask().execute(this.getApplicationInfo().dataDir + "/files/" + newUserRegistrationFilename);
    
    
        }
    
        // Function to check and request permission.
        public void checkPermission(String permission, int requestCode) {
            if (ContextCompat.checkSelfPermission(MainActivity.this, permission) == PackageManager.PERMISSION_DENIED) {
    
                // Requesting the permission
                ActivityCompat.requestPermissions(MainActivity.this, new String[] { permission }, requestCode);
            }
            else {
                Toast.makeText(MainActivity.this, "Permission already granted", Toast.LENGTH_SHORT).show();
            }
        }
    
        void showToast(String textInput, int duration) {
            Context context = getApplicationContext();
            CharSequence text = textInput;
            Toast toast = Toast.makeText(context, text, duration);
            toast.show();
        }
    
        //  AsyncTask must be subclassed to be used. The subclass will override at least one method
        //  `doInBackground(Params...)`, and most often will override a second one `onPostExecute(Result)`
        //  Reference: https://developer.android.com/reference/android/os/AsyncTask?authuser=4
        //  Reference: https://stackoverflow.com/a/12447497/6667035
        private class FtpTask extends AsyncTask<String, Void, Boolean> {
            //  `doInBackground` invoked on the background thread immediately after `onPreExecute()`
            //  finishes executing. This step is used to perform background computation
            //  that can take a long time. The parameters of the asynchronous task are
            //  passed to this step. The result of the computation must be returned by
            //  this step and will be passed back to the last step. This step can also use
            //  `publishProgress(Progress...)` to publish one or more units of progress.
            //  These values are published on the UI thread, in the `onProgressUpdate(Progress...)`
            //  step.
            protected Boolean doInBackground(String... filenames) {
                FtpHostProfiles ftpHostProfiles = new FtpHostProfiles();
                ftpHostProfiles.addProfile(new FtpHostProfile(
                        "Profile1",
                        "Hostname1",
                        21,
                        "Username1",
                        "Password1"));
                ftpHostProfiles.addProfile(new FtpHostProfile(
                        "Profile2",
                        "Hostname2",
                        21,
                        "Username2",
                        "Password2"));
    
                FtpConnection ftpConnect = new FtpConnection(ftpHostProfiles);
                for (int i = 0; i < filenames.length; i++) {
                    Log.v("FTPTask","Preparing to upload " + filenames[i]);
                    var result  = ftpConnect.uploadFile("Profile1", filenames[i]);
                    if (result == false) {
                        Log.v("FTPTask","Fail to upload " + filenames[i] + "!");
                        return false;
                    } else {
                        Log.v("FTPTask","Upload " + filenames[i] + " successfully!");
                    }
                    if (isCancelled()) break;
                }
                return true;
            }
    
            //  `onPostExecute` invoked on the UI thread after the background computation finishes.
            //  The result of the background computation is passed to this step as
            //  a parameter.
            protected void onPostExecute(boolean result) {
                if (result) {
                    Log.v("FTPTask","FTP upload complete");
                }
                return;
            }
        }
    }
    
  • User class implementation:

    package com.example.ftpreplycodeclass;
    
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    
    public class User  implements java.io.Serializable {
        private String fullName;
        private String personalID;
        private String dateOfBirth;
        private String cellPhoneNumber;
        private String emailInfo;
        private String password;
    
    
        public User(final String fullNameInput,
                    final String personalIDInput,
                    final String dateOfBirthInput,
                    final String cellPhoneNumberInput,
                    final String emailInfoInput,
                    final String passwordInput) {
            fullName = fullNameInput;
            personalID = personalIDInput;
            dateOfBirth = dateOfBirthInput;
            cellPhoneNumber = cellPhoneNumberInput;
            emailInfo = emailInfoInput;
            try {
                password = hashingMethod(passwordInput);
            } catch (Exception ex) {
                password = passwordInput;
            }
            return;
        }
    
        public String getFullName() {
            return fullName;
        }
    
        public String getPersonalID() {
            return personalID;
        }
    
        public String getDateOfBirth() {
            return dateOfBirth;
        }
    
        public String getCellPhoneNumber() {
            return cellPhoneNumber;
        }
    
        public String getEmailInfo() {
            return emailInfo;
        }
    
        public String getHash() throws NoSuchAlgorithmException {
            return hashingMethod(fullName + personalID);
        }
    
        public String getHashedPassword() throws NoSuchAlgorithmException {
            return password;
        }
    
        public boolean checkPassword(String password) {
            boolean result = false;
            try {
                result = password.equals(hashingMethod(password));
            }
            catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        //**********************************************************************************************
    
        //  Reference: https://stackoverflow.com/a/2624385/6667035
        private String hashingMethod(String inputString) throws NoSuchAlgorithmException {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            String stringToHash = inputString;
            messageDigest.update(stringToHash.getBytes());
            String stringHash = new String(messageDigest.digest());
            return stringHash;
        }
    }
    
  • Serialization class implementation:

    package com.example.ftpreplycodeclass;
    
    import android.content.Context;
    import android.util.Log;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    //  Reference: https://codereview.stackexchange.com/q/260909/231235
    public class Serialization {
    
        //  Reference: https://stackoverflow.com/a/4118917/6667035
        //  fileName cannot contain any path separator
        public static boolean Save(Context context, String fileName, User user) {
            try {
                FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
                String fullFilename = context.getFilesDir() + "/" + fileName;
                Log.d("Save", "Save file in " + fullFilename);
                ObjectOutputStream os = new ObjectOutputStream(fos);
                os.writeObject(user);
                os.close();
                fos.close();
                if (new File(fullFilename).exists()) {
                    return true;
                } else {
                    return false;
                }
            } catch (IOException i) {
                i.printStackTrace();
                return false;
            }
        }
    
        public static User Load(Context context, String fileName) {
            try {
                FileInputStream fis = context.openFileInput(fileName);
                ObjectInputStream is = new ObjectInputStream(fis);
                User simpleClass = (User) is.readObject();
                is.close();
                fis.close();
                return simpleClass;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
  • FtpHostProfile class implementation:

    package com.example.ftpreplycodeclass;
    
    public final class FtpHostProfile {
        private final String profileName;
        private final String ftpHostName;
        private final int ftpPort;
        private final String ftpUserName;
        private final String ftpPassword;
    
        public FtpHostProfile(String hostNameInput, int portInput, String userNameInput, String passwordInput) {
            this.profileName = "";
            this.ftpHostName = hostNameInput;
            this.ftpPort = portInput;
            this.ftpUserName = userNameInput;
            this.ftpPassword = passwordInput;
        }
        public FtpHostProfile(
                String profileNameInput,
                String hostnameInput,
                int portInput,
                String usernameInput,
                String passwordInput) {
            this.profileName = profileNameInput;
            this.ftpHostName = hostnameInput;
            this.ftpPort = portInput;
            this.ftpUserName = usernameInput;
            this.ftpPassword = passwordInput;
        }
    
        public String getProfileName() {
            return this.profileName;
        }
    
        public String getHostname() {
            return this.ftpHostName;
        }
    
        public int getPort() {
            return this.ftpPort;
        }
    
        public String getUsername() {
            return this.ftpUserName;
        }
    
        public String getPassword() {
            return this.ftpPassword;
        }
    }
    
  • FtpHostProfiles class implementation:

    package com.example.ftpreplycodeclass;
    
    import android.util.Log;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    public final class FtpHostProfiles {
        private final Map<String, FtpHostProfile> profiles = new HashMap<>();
    
        public FtpHostProfiles() {
            // Empty constructor
        }
    
        public FtpHostProfiles addProfile(FtpHostProfile input) {
            this.profiles.put(input.getProfileName(), input);
            return this;
        }
    
        public FtpHostProfiles addProfiles(Collection<FtpHostProfile> input) {
            input.stream().forEach(profile -> profiles.put(profile.getProfileName(), profile));
            return this;
        }
    
        public FtpHostProfiles addProfiles(FtpHostProfiles input) {
            this.profiles.putAll(input.profiles);
            return this;
        }
    
        public FtpHostProfile getProfile(String profileNameInput) {
            FtpHostProfile profile = profiles.get(profileNameInput);
            if (profile == null) {
                Log.d("GetProfile", "No such item in stored profiles");
                throw new IllegalStateException("No such item in stored profiles");
            }
            return profile;
        }
    
    }
    
  • AndroidManifest.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.ftpreplycodeclass">
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.FtpReplyCodeClass">
            <activity
                android:name=".MainActivity"
                android:exported="true">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
        <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    </manifest>
    

All suggestions are welcome.

The summary information:

\$\endgroup\$
1
\$\begingroup\$

FtpConnection

  • You're duplicating connectftp in uploadFile. uploadFile should probably call connectftp rather than re-implement it itself
  • On that note, consider whether or not it makes sense to have construction and connection be two separate steps. While that is often a valid approach, having your class represent an open connection may sometimes be more natural
  • The name connectftp is a bit redundant, isn't it? Having an FtpConnection class with a connect method seems clear enough to me

FtpReplyCode

  • I'm not sure "" is an appropriate response for unknown reply codes. If your program doesn't understand what the FTP server is asking for, I think that should be made more obvious. Maybe with an exception, or maybe by having getReplyMessage return an Optional<String> instead of a String.
  • While there's nothing wrong with having a chain of ifs like that, you're really just mapping some inputs to some outputs in a really straightforward way. Having a lookup table in the form of a Map<Integer, String> could be more flexible

User

  • I'm not sure if silently storing the password un-hashed if hashing fails is the best idea. It doesn't seem likely to fail, but if it does, you'd probably want to know
  • Why is getHashedPassword declared as throws NoSuchAlgorithmException?

Serialization

  • You can get the file path using context.getFileStreamPath(fileName)
  • If something goes wrong during saving or loading, you might not end up closing the input/output streams. You might want to use try-with-resources statements to make sure they do get closed
  • Method names should usually begin with a lowercase letter, so save and load rather than Save and Load

FtpHostProfile / FtpHostProfiles

  • Do the profiles need to know their own names? Maybe they do, but if they don't, you might be able to have the names only exist in the context of a FtpHostProfiles, which could then go from needing a distinct class to just being a Map<String, FtpHostProfile>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.