DEV Community

Cover image for Developing an Arabic Learning Terminal-Based App!
TishkSuran
TishkSuran

Posted on • Updated on

Developing an Arabic Learning Terminal-Based App!

Backstory

Since I am currently in the process of self-teaching Arabic, I thought this project would be a good idea. It has allowed me to combine two of my hobbies: languages and programming, and overall, it has been a very enjoyable experience. Being new to Java, this project has enabled me to explore more complex programming features such as JUnit testing, encapsulation, inheritance, polymorphism, etc. Additionally, I've had the opportunity to learn some more Arabic words. A definite win-win!


Brief Overview

This project can be broken down into five main components.

  1. Secure Sign-up Process: Validates user input, ensures data integrity, and securely stores user data using SHA-256 encryption. This information is critical for logins, leaderboard creation, and XP tracking.
  2. Interactive Arabic Flashcards: Three sets of flashcards that display a word, it's Arabic spelling, it's phonetic pronunciation, it's English translation, and audible pronunciation.
  3. Arabic Listening Exams: Three listening exams based off the words presented in the flashcards. Users listen to a word and write its English meaning. Completion unlocks more challenging modes categorised as beginner, intermediate, and advanced.
  4. Global Leaderboard: Features an XP system where users earn XP by completing tests and studying flashcards. Users can compare XP on the global leaderboard.
  5. JUnit Testing: Extensive JUnit testing for validation methods as well as both listening tests and interactive flashcards.

Secure Sign-up Process:

At the heart of this application is a secure sign up process, it is composed of five major methods:

  1. registerUser
  2. updateExperiencePoints
  3. updateUserProficiency
  4. loginUser
  5. hashPassword

For the sake of this section, we will focus on the implementation of the registerUser, loginUser and hashPassword methods. The two other method will be spoke about later on in this blog.


Register User Method:

Gathering User Information:
The core functionality of the registerUser is to collect user data such as first name, last name, username, email, self declared Arabic proficiency level, and password. Leveraging the Scanner class, to capture user input and store it to it's corresponding variable for further processing.


Validating User Inputs:
Validation mechanisms were crucial for this project to ensure data integrity and prevent potential issues such as null pointers or conflicts with usernames or emails. Such issues could have easily arisen during user sign-in processes if it was not for the use of validation methods.

// Method to check if the provided password matches the hashed password
public static boolean checkPassword(String password, String hashedPassword) {
        return hashPassword(password).equals(hashedPassword);
    }

    // Regular expression pattern for validating name
    public static final Pattern VALID_NAME_REGEX = Pattern.compile("^[a-zA-Z]*$", Pattern.CASE_INSENSITIVE);

    // Method to validate first name
    public static boolean isValidFirstName(String firstName) {
        Matcher matcher = VALID_NAME_REGEX.matcher(firstName);
        return matcher.matches();
    }

    // Method to validate second name
    public static boolean isValidSecondName(String secondName) {
        Matcher matcher = VALID_NAME_REGEX.matcher(secondName);
        return matcher.matches();
    }

    // Method to validate password
    public static boolean isValidPassword(String password) {
        return password.length() >= 8 && password.matches(".*\\d.*") && password.matches(".*[A-Z].*") && password.matches(".*[a-z].*");
    }

    // Regular expression pattern for validating email
    public static final Pattern VALID_EMAIL_ADDRESS_REGEX = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);

    // Method to validate email
    public static boolean isValidEmail(String email) {
        Matcher matcher = VALID_EMAIL_ADDRESS_REGEX.matcher(email);
        return matcher.matches();
    }

    // Method to check if email is already registered
    public static boolean isEmailAlreadyRegistered(String email) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(CSV_FILE))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",");
                if (parts.length >= 1 && parts[0].equals(email)) {
                    return true;
                }
            }
        }
        return false;
    }

    // Method to check if username is already registered
    public static boolean isUsernameAlreadyRegistered(String username) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(CSV_FILE))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",");
                if (parts.length >= 5 && parts[5].equals(username)) {
                    return true;
                }
            }
        }
        return false;
    }
Enter fullscreen mode Exit fullscreen mode



Enhancement of User Experience:
To streamline the registration process, I prioritised user experience enhancements, I implemented intuitive prompts and informative error messages, empowering users to navigate registration, whilst also providing clear instructions for error resolutions.

// Validating email
email = "";
while (email.isEmpty() || !isValidEmail(email) || isEmailAlreadyRegistered(email)) {
    System.out.println("Enter your email address: ");
    email = scanner.nextLine();
    if (email.isEmpty()) {
        System.out.println("This field cannot be left blank.");
    } else if (!isValidEmail(email)) {
        System.out.println("Invalid email address. Please use a valid email address.");
    } else if (isEmailAlreadyRegistered(email)) {
        System.out.println("Email already exists. Please login or choose a different email address.");
    }
}

// Validating password
String password = "";
while (password.isEmpty() || !isValidPassword(password)) {
    System.out.println("Please create a password: ");
    password = scanner.nextLine();
    if (password.isEmpty()) {
        System.out.println("This field cannot be left blank.");
    } else if (!isValidPassword(password)) {
        System.out.println("Invalid password. Password must be at least 8 characters long and contain at least one digit, one uppercase letter, and one lowercase letter.");
    }
}
Enter fullscreen mode Exit fullscreen mode



Saving User Information:
Persistence of user data/user input was crucial. In order to achieve persistence of data, I integrated file handling capabilities to serialise user information into a CSV format. User details were stored as discrete records within the file for efficient management and retrieval. The persistence of this data allows for features such as login, XP tracking, and also memory of set proficiency level and thus mode of application.

        // Initial experience points set to 0 for all users
        int experiencePoints = 0;

        // Writing user information to CSV file
        try (PrintWriter writer = new PrintWriter(new FileWriter(CSV_FILE, true))) {
            writer.println(email + "," + hashedPassword + "," + firstName + "," + secondName + "," + proficiency + "," + userName + "," + experiencePoints);
            System.out.println("User registered successfully.");

            // Setting the registered email
            Login_System.email = email;
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
Enter fullscreen mode Exit fullscreen mode

Login User Method:

Prompting User Input:
The loginUser method initiates by requesting user credentials, in which they have the choice to either provide the email or the username alongside the password they inputted within the registerUser method.

System.out.println("Enter your email or username: ");
String userInput = scanner.nextLine();
System.out.println("Enter your password: ");
String password = scanner.nextLine();
Enter fullscreen mode Exit fullscreen mode



Reading User Data from CSV:
Utilising a BufferedReader, the program is able to read through the CSV file which holds user information, filtering the lines based on user input. The Streams API facilitates streamlined processing as well as enhancing efficiency. Although slightly overkill for it's purpose, it was still enjoyable using Java functionality I had never used before for this project.

try (BufferedReader reader = new BufferedReader(new FileReader(CSV_FILE))) {
    Stream<String> lines = reader.lines();
    lines.filter(line -> {
                String[] parts = line.split(",");
                return parts.length == 7 && (parts[0].equals(userInput) || parts[5].equals(userInput));
            })
            // Further processing...
Enter fullscreen mode Exit fullscreen mode



Validating User Credentials:
Once a matching record is found, the stored hashed password is compared against that of the user's input. If the password check passes, authentication succeeds, enabling user access. Upon successful authentication, pertinent user details such as first name, proficiency level, and experience points are extracted from the CSV record and presented to the user.

String[] parts = line.split(",");
String storedHashedPassword = parts[1];
if (checkPassword(password, storedHashedPassword)) {
    // Authentication success, further processing...
} else {
    System.out.println("Invalid password.");
}
Enter fullscreen mode Exit fullscreen mode
String firstName = parts[2];
String proficiency = parts[4];
int experiencePoints = Integer.parseInt(parts[6]);
System.out.println("Welcome, " + firstName + "! Your Arabic proficiency level is currently set to: " + proficiency + ", and your current XP is: " + experiencePoints + ".");
Enter fullscreen mode Exit fullscreen mode



Error Handling and Exception Management:
If the provided username, email or password is incorrect, an error message will be provided, to achieve this functionality, I have deployed the use of a lambda method for greater conciseness and readability.

() -> System.out.println("Invalid email or username."));
} catch (IOException e) {
    System.out.println("Error: " + e.getMessage());
}
Enter fullscreen mode Exit fullscreen mode

Secure Password Storage:

Hashing Passwords:
The method I used in order to hash user inputed passwords was the SHA-256 hashing algorithm, known for its cryptographic strength and resistance to pre-image attacks. Upon receiving a plaintext password, it computes its hash digest using the SHA-256 algorithm, converting the resulting byte array into a hexadecimal representation.

public static String hashPassword(String password) {
    try {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = md.digest(password.getBytes());
        StringBuilder sb = new StringBuilder();
        for (byte b : hashBytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode



Algorithm Selection and Message Digest Initialisation:

MessageDigest md = MessageDigest.getInstance("SHA-256");
Enter fullscreen mode Exit fullscreen mode

By invoking getInstance method with the SHA-256 identifier, the method acquires an instance of the SHA-256 message digest algorithm. This initialisation step establishes a secure cryptographic context for hashing operations, ensuring the integrity of the password hashing process.


Byte Array Conversion:

byte[] hashBytes = md.digest(password.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
    sb.append(String.format("%02x", b));
}
return sb.toString();
Enter fullscreen mode Exit fullscreen mode

The method computes the hash digest of the input password by invoking the digest method on the MessageDigest instance. This operation yields a byte array representing the hashed password. Subsequently, the method iterates through each byte of the hash digest, converting it to its hexadecimal representation and appending it to a StringBuilder. Finally, the method returns the hexadecimal string representation of the hashed password.


Side Note:
In order to gain a comprehensive understanding of the security features inherent in the SHA-256 hash method, I highly recommend watching this video: SHA-256 Explained - 3Blue1Brown.


Interactive Arabic Flashcards:

Architecture Overview:
The Arabic Flashcards application is structured around an abstract base class Arabic_Flashcards_Base and three concrete subclasses: Arabic_Flashcards_Beginner, Arabic_Flashcards_Intermediate, and Arabic_Flashcards_Advanced. Each subclass caters to users of different proficiency levels, offering distinct sets of flashcards with varying complexities.

public class Arabic_Flashcards_Intermediate extends Arabic_Flashcards_Base {
    public Arabic_Flashcards_Intermediate() {
        super(new String[]{"Favourite.wav", "Good_morning.wav", "I_love_you.wav", "Interesting.wav", "No_problem.wav", "Of_course.wav", "Thank_you_very_much.wav", "Thats_good.wav", "What_is_that.wav", "You_are_welcome.wav"},
                "src/Arabic_Words/Arabic_Words_Intermediate",
                new String[]{
                        "Favourite",
                        "Good morning",
                        "I love you",
                        "Interesting",
                        "No problem",
                        "Of course",
                        "Thank you very much",
                        "That's good",
                        "What is that",
                        "You are welcome"},
                new String[]{
                        "Ālmufaddal",
                        "Sabāĥu al-khayr'",
                        "'Anā uĥibbuk",
                        "Muthīrun lil-ihtimām",
                        "Lā mushkilah",
                        "Bi-ttab'",
                        "Shukran jazīlan",
                        "Hādhā jayyid",
                        "Mā hadhā",
                        "'Āfwān"},
                new String[]{
                        "المفضل",
                        "صباح الخير",
                        "أنا أحبك",
                        "مثير للاهتمام",
                        "لا مشكلة",
                        "بالطبع",
                        "شكرا جزيلا",
                        "هذا جيد",
                        "ما هذا",
                        "عفوا"
                });
    }
}
Enter fullscreen mode Exit fullscreen mode

User Interaction:
Upon instantiation, each subclass initialises the flashcard data and provides methods for starting flashcard practice. Users are prompted to select a flashcard by inputting an integer corresponding to the desired word. Subsequently, the application plays the audio pronunciation and displays relevant information, such as English translation, phonetic pronunciation, and the Arabic word itself.

public void startFlashCardPractice() {
    Scanner scanner = new Scanner(System.in);
    // Retrieve user's email which is later used to update XP
    String email = Login_System.email;
    System.out.println("Welcome to Arabic Word Learning");
    // Display flashcard options
    for (int i = 0; i < 10; i++) {
        System.out.println((i + 1) + ". " + arabicWordsInEnglish[i]);
    }
    System.out.println("0. Exit");
    System.out.println();
    System.out.println("Please input an integer from 0 to 10 to hear the corresponding word along with its phonetic pronunciation.");

    while (true) {
        int userInput = scanner.nextInt();
        if (userInput == 0) {
            System.out.println("Exiting...");
            return;
        } else if (userInput < 0 || userInput > 10) {
            System.out.println("Invalid choice. Please enter a number between 0 and 10.");
            continue;
        };

        // Retrieve corresponding audio file
        String audioFile = audioFiles[userInput - 1];
        // Play the audio
        Play_Audio.playAudio(audioDirectory + File.separator + audioFile);

        // Update experience points based on user's selection
        switch (userInput) {
            case 1:
                System.out.println("| " + arabicWordsInEnglish[0] + " | " + arabicWordsPhonetic[0] + " | " + arabicWords[0] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 2:
                System.out.println("| " + arabicWordsInEnglish[1] + " | " + arabicWordsPhonetic[1] + " | " + arabicWords[1] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 3:
                System.out.println("| " + arabicWordsInEnglish[2] + " | " + arabicWordsPhonetic[2] + " | " + arabicWords[2] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 4:
                System.out.println("| " + arabicWordsInEnglish[3] + " | " + arabicWordsPhonetic[3] + " | " + arabicWords[3] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 5:
                System.out.println("| " + arabicWordsInEnglish[4] + " | " + arabicWordsPhonetic[4] + " | " + arabicWords[4] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 6:
                System.out.println("| " + arabicWordsInEnglish[5] + " | " + arabicWordsPhonetic[5] + " | " + arabicWords[5] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 7:
                System.out.println("| " + arabicWordsInEnglish[6] + " | " + arabicWordsPhonetic[6] + " | " + arabicWords[6] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 8:
                System.out.println("| " + arabicWordsInEnglish[7] + " | " + arabicWordsPhonetic[7] + " | " + arabicWords[7] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 9:
                System.out.println("| " + arabicWordsInEnglish[8] + " | " + arabicWordsPhonetic[8] + " | " + arabicWords[8] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
            case 10:
                System.out.println("| " + arabicWordsInEnglish[9] + " | " + arabicWordsPhonetic[9] + " | " + arabicWords[9] + " |");
                Login_System.updateExperiencePoints(email, 5);
                break;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration with Login System:
This part of the application integrates with the login system, enabling user specific interactions and experience point tracking. Upon flashcard selection, the application updates the user's XP based on how many flashcards they view, enhancing gamification. We will discuss how the XP system works later on within this blog.


Arabic Listening Exams:

Architecture Overview:
Similar to that of the flashcard's architecture, the Arabic listening tests are structured around a modular architecture comprising several classes, each catering to distinct levels of proficiency: Arabic_Listening_Test_Beginner, Arabic_Listening_Test_Intermediate, and Arabic_Listening_Test_Advanced. This allows for the customisation of listening tests according to the user's skill level.

// Class hierarchy for different proficiency levels
public class Arabic_Listening_Test_Base { ... }
public class Arabic_Listening_Test_Beginner extends Arabic_Listening_Test_Base { ... }
public class Arabic_Listening_Test_Intermediate extends Arabic_Listening_Test_Base { ... }
public class Arabic_Listening_Test_Advanced extends Arabic_Listening_Test_Base { ... }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)