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:
The Java Streams API, introduced in Java 8 (released in March 2014), is a powerful tool for processing sequences of elements in a functional style. It allows for operations on collections of objects in a declarative manner, similar to SQL statements. The Streams API provides a more readable and concise way to perform bulk operations on data, such as filtering, mapping, and reducing.

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 and enhances efficiency. While it may be slightly overkill for this particular task, it was still enjoyable to use Java functionality I had never explored before for this project.

Here’s the code snippet demonstrating the use of Streams API to read and process the CSV file:

try (BufferedReader reader = new BufferedReader(new FileReader(CSV_FILE))) {
    // Using Streams API to process lines of the 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));
            }).findFirst()
            .ifPresentOrElse(line -> {
                String[] parts = line.split(",");
                String storedHashedPassword = parts[1];
                if (checkPassword(password, storedHashedPassword)) {
                    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 + ".");
                    Login_System.experiencePoints = experiencePoints;
Enter fullscreen mode Exit fullscreen mode

Benefits of Using Streams API to Read User Data from CSV:

  1. Readability and Maintainability: The Streams API provides a clean and declarative way to handle the CSV data. The filter, findFirst, and ifPresentOrElse methods clearly express the intent of the code, making it easier to read and maintain.
  2. Conciseness: Using the Streams API reduces boilerplate code. Traditional approaches would require multiple loops and condition checks, whereas the Streams API accomplishes this in fewer lines of code.
  3. Error Handling: The Streams API integrates smoothly with modern Java error handling. The code uses try-with-resources to ensure the BufferedReader is closed properly, and handles exceptions in a streamlined manner.



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

Global Leaderboard:

Updating User Experience Points:
The entirety of this project's XP system is based around this method. This method updates the experience points of the user, and persistence is obtained since it writes the new XP into the CSV file which holds user information. The method reads the CSV file line by line, and if it finds a line where the first or sixth field matches the provided email, it adds the new experience points to the current ones. The updated data is then written back to the CSV file. If any file reading or writing error occurs, it prints the stack trace of the exception.

    // Method to update experience points for a user
    public static void updateExperiencePoints(String email, int newXP) {
        try (BufferedReader reader = new BufferedReader(new FileReader(CSV_FILE))) {
            String line;
            StringBuilder fileContent = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",");
                if (parts.length == 7 && (parts[0].equals(email) || parts[5].equals(email))) {
                    int currentXP = Integer.parseInt(parts[6]);
                    currentXP += newXP;
                    parts[6] = String.valueOf(currentXP);
                    line = String.join(",", parts);
                }
                fileContent.append(line).append("\n");
            }

            try (FileWriter writer = new FileWriter(CSV_FILE)) {
                writer.write(fileContent.toString());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Enter fullscreen mode Exit fullscreen mode

The Stages Where User's can Gain XP:
Users can earn XP in the two different modes of this application: they can gain XP from practicing flashcards alone, or they can also earn XP through exams. Their score will determine the amount of XP they gain.

    // Method to display test results and update experience points
    protected void displayResults(int correctAnswerCount) {
        int experiencePoints = Login_System.experiencePoints;
        String email = Login_System.email;
        if (correctAnswerCount == audioFiles.length) {
            System.out.println("Well done! You've achieved a flawless score, earning you 50 experience points.");
            Login_System.updateExperiencePoints(email, 50);
        } else if (correctAnswerCount >= 8) {
            System.out.println("You have earned 25 experience points.");
            Login_System.updateExperiencePoints(email, 25);
        } else if (correctAnswerCount >= 5) {
            System.out.println("You have earned 10 experience points.");
            Login_System.updateExperiencePoints(email, 10);
        }
        System.out.println();
    }
Enter fullscreen mode Exit fullscreen mode

Leaderboard System:
The Leaderboard class is a stanalone utility for generating and displaying the leaderboard based on users XP. It relies on the data stored within the CSV file which is managed by the Login_System to populate the leaderboard. Once all the user data is collected from the CSV file, it is sorted into a list based on the userXP attribute in descending order. This sorting is achieved using Collections.sort() with a custom comparator that compares Leader objects based on their XP values.

class Leader {
    String userName;
    int userXP;

    public Leader(String userName, int userXP) {
        this.userName = userName;
        this.userXP = userXP;
    }
}

public class Leaderboard {
    static String CSV_FILE = Login_System.CSV_FILE;

    public static void Leaderboard() {
        List<Leader> leaders = new ArrayList<>();

        try (BufferedReader reader = new BufferedReader(new FileReader(CSV_FILE))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",");
                if (parts.length == 7) {
                    int userXP = Integer.parseInt(parts[6]);
                    String userName = parts[5];
                    leaders.add(new Leader(userName, userXP));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        Collections.sort(leaders, Comparator.comparingInt((Leader l) -> l.userXP).reversed());

        int n = 1;
        System.out.println("|ARABIC LEADERBOARD|");
        for (Leader leader : leaders) {
            System.out.println(n + ") " + leader.userName + " | XP: " + leader.userXP);
            n++;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

JUnit Testing:

Ensuring the reliability and functionality of the software was paramount in the development of the Arabic Listening Tests and the Login System. With numerous audio files and transcription validations in the Arabic Listening Tests, and the intricate input validation requirements in the Login System, manual testing of every scenario would have been impractical. This is where the use of JUnit testing proved indispensable.

JUnit was deployed extensively throughout the codebase, particularly in validating critical functionalities such as audio transcription accuracy and user input validation. Specifically, JUnit was instrumental in testing the main methods, login and registration processes, and examination modules, ensuring their robustness and correctness.

For example, let us take a closer look at one of the test cases from the Arabic Listening Test modules:

@Test
public void testCorrectTranscriptionThatsGood() {
    String expected = "That's good";
    String actual = testIntermediate.getCorrectTranscription("Thats_good.wav");
    assertEquals(expected, actual);
}
Enter fullscreen mode Exit fullscreen mode

In this test case, JUnit validates the accuracy of the transcription for the audio file "Thats_good.wav." The expected transcription is "That's good," and JUnit compares it against the actual transcription obtained from the getCorrectTranscription() method of the testIntermediate object. If the actual transcription matches the expected one, the test case passes, indicating the correctness of the transcription process.

This demonstrates how JUnit enables meticulous validation of audio transcriptions, ensuring precision and accuracy in the Arabic Listening Tests module. Similar test cases, covering various audio files and transcription scenarios, contribute to the overall reliability and quality of the software solution.

Top comments (0)