4
\$\begingroup\$

I'm working on a simple method that would censor emails (GPDR stuff) so they won't show the full name of a user, with 1st character, optional dot, 1st character after the dot and whatever is left after the @ to be left uncensored. So, the intent is like this:

Email to be censored: name.lastname@email.com

Desired effect: n***.l*******@email.com

I've written this:

public static String getCensoredUsername(String username) {
    if (username.contains("@")) {
        char[] chars = username.toCharArray();
        for (int i = 1; i < chars.length; i++) {
            boolean isCurrentCharacterADot = chars[i] == '.';
            boolean isPreviousCharacterADot = chars[i - 1] == '.';

            if (chars[i] == '@') {
                break;
            }
            if (chars[i] == '.') {
                chars[i] = '.';
            } else if (!isCurrentCharacterADot && !isPreviousCharacterADot) {
                chars[i] = '*';
            }
        }
        return String.valueOf(chars);
    }
    return username;
}

It feels to me very Java 6. What would make it look better?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Please take into account that here on Code Review answers won't come as quick as on Stack Overflow, hence you should really wait at least a day before you accept an answer althought it is mine ;-) \$\endgroup\$
    – Heslacher
    Apr 20, 2021 at 13:06
  • \$\begingroup\$ ok, noted. thank you for your help! \$\endgroup\$ Apr 20, 2021 at 13:20
  • \$\begingroup\$ Feel free to unaccept my answer this could help to get more answers which may be better as well. \$\endgroup\$
    – Heslacher
    Apr 20, 2021 at 13:22
  • \$\begingroup\$ I get the feeling that you're hiding information but you want to retain some information in them so that the addresses can still be identified. This doesn't seem to comply with GDPR too well. If you want to protect privacy, just replace the shown e-mail addresses with an UUID and keep the e-mail -> UUID mapping in a protected database. As Fabio F. showed, your approach fails with common e-mail addresses leaving you vulnerable to GDPR violations so that too speaks against "e-mail address sanitation." (Instead of UUID you can use any shorter non-reversible and non-guessable random unique token.) \$\endgroup\$ Apr 21, 2021 at 4:33

3 Answers 3

3
\$\begingroup\$

Just a few remarks....

  • if username doesn't contain a @ the code should return early which saves one level of indentation.
  • The condition if (chars[i] == '.') should better be if (isCurrentCharacterADot) but you could omit this as well because you won't need to assign anything to chars[i] if it is a dot.

This could look like so

public static String getCensoredUsername(String username) {
    if (!username.contains("@")) { return username; }

    char[] chars = username.toCharArray();
    for (int i = 1; i < chars.length; i++) {
        boolean isCurrentCharacterADot = chars[i] == '.';
        boolean isPreviousCharacterADot = chars[i - 1] == '.';

        if (chars[i] == '@') {
            break;
        }
        else if (!isCurrentCharacterADot && !isPreviousCharacterADot) {
            chars[i] = '*';
        }
    }
    return String.valueOf(chars);
}

As a side note, because this is a public method there should be some parameter-validation.

\$\endgroup\$
7
\$\begingroup\$

There is nothing wrong about being "too Java 6"!

First few notes about naming and naming convention:

  1. username is, in fact, the email, so should be email
  2. getCensoredUsername for the same reason should be getCensoredEmail but in Java get, by convention, is used only for getter or when you're applying very little logic; thus is better to use an "action-subject" type of name like censorEmail
  3. Being Java a type language, calling a variable chars is not helpful, that should be emailChars or even just email if that is not colliding with other variables, the compiler will tell you the type.

Biggest problem though, is the data validation, you should check at least that email is not null.

Normally you would check that the email is "valid" but if you take a look at the email syntax you might notice that's almost impossible to do it without writing a formal parser (or using some library). So let's say we want just censor the "reasonably valid" emails.

Your approach to go char by char in the username part is not very flexible: what if tomorrow you want to have f****o.f*****i@gmail.com? You need to add other flags, and wouldn't be very enjoyable.

I suggest you to start from the censor method and build bottom up from that.

Here is my solution

  public static String hideLastChars(String name) {
    Objects.requireNonNull(name, "String should not be null");
    if (name.length() <= 1) {
      return name;
    } else {
      return name.charAt(0) + IntStream.range(1, name.length()).mapToObj(i -> "*").collect(Collectors.joining());
    }
  }

Now that we have our "logic" we need to do a bit of input validation.

  public static String censorEmail(String email) {
    Objects.requireNonNull(email, "Email should not be null");
    String[] splitEmail = email.split("@");
    if (splitEmail.length > 1 && splitEmail[0].length() > 0) {
      return censorUsername(splitEmail[0]) + "@" + splitEmail[1];
    }
    throw new IllegalArgumentException("Please provide a valid email");
  }

Now we know that the email is not null, it does contain a '@' and user is there. We just need to apply our hideLastChars to each piece.

We have a little problem here because if you just split on each . you will end up losing trailing dots, for example in an email like john.doe....@gmail.com. A solution is to use Lookahead regexp to split on each . but keeping it in the split result, this is the regexp: ((?<=\.)|(?=\.)), for example "john.doe...".split("((?<=\.)|(?=\.))") will return { "john", ".", "doe", ".", ".", "." }

Now we know how to map each piece and how to extract it.

  private static String censorUsername(String username) {
    return Arrays
        .stream(username.split("((?<=\\.)|(?=\\.))"))
        .map(scratch_1::hideLastChars)
        .collect(Collectors.joining(""));
  }

I suggest you to create a unit test to test the logic so every time you find a new email that creates problem you can add in the test and see what happens (What about p.g@curry.com?). I've run few examples using your code and mine. Here's the result

INPUT ORIGINAL REVIEWED
name.surname@gmail.com n***.s******@gmail.com n***.s******@gmail.com
name.surname.p99@gmail.com n***.s******.p**@gmail.com n***.s******.p**@gmail.com
name.surname.p99.@gmail.com n***.s******.p**.@gmail.com n***.s******.p**.@gmail.com
@gmail.com @*****.c** Exception: Please provide a valid email
user.com user.com Exception: Please provide a valid email
user%example.com@example.org u***********.c**@example.org u***********.c**@example.org
"john..doe..."@example.org "****..d**..."@example.org "****..d**..."@example.org
" "@example.org "**@example.org "**@example.org
null Exception: Cannot invoke "String.contains(java.lang.CharSequence)" because "username" is null Exception: Email should not be null
...p...g...@gmail.com ...p...g...@gmail.com ...p...g...@gmail.com
p.g@curryfication.com p.g@curryfication.com p.g@curryfication.com
\$\endgroup\$
1
  • 2
    \$\begingroup\$ The "p.g@example.com" is a great example of how this approach is not going to work too well in practise. The sanitation scheme also leaves unique addresses fully exposed. I know people who own theirown domains and have "firstname@lastname.tld" addresses. Removing the first name does not make the address any less unique. \$\endgroup\$ Apr 21, 2021 at 4:39
1
\$\begingroup\$

If username is not an email, getCensoredUsername(username) does nothing, which is a little bit confusing.

Two possible approaches:

  1. Call the method censorLocalPart(String email) and use it only if the username is an email. (this suggestion is on the lines of @Fabio F.'s review)
  2. Add a comment (or Javadoc) to specify that a username will be censored only if it is an email.

An alternative is to use replaceAll with a regex:

public static String censorUsername(String username){
    return username.replaceAll("(?<=[^\\.])[^\\.](?=.*@)","*");
}

Regex demo

A brief explanation of the regex:

  • (?<=[^\.]): lookbehind for a character that is not .
  • [^\.]: match a character that is not .
  • (?=.*@): make sure that there is an @ on the right. This is to exclude the domain of the email and usernames that are not emails.

Some tests (borrowed from @Fabio F.):

@Test
public void testCensorUsername() {
    assertEquals("simpleUsername", censorUsername("simpleUsername"));
    assertEquals("n***.s******@gmail.com", censorUsername("name.surname@gmail.com"));
    assertEquals("n***.s******.p**@gmail.com", censorUsername("name.surname.p99@gmail.com"));
    assertEquals("n***.s******.p**.@gmail.com", censorUsername("name.surname.p99.@gmail.com"));
    assertEquals("u***********.c**@example.org", censorUsername("user%example.com@example.org"));
    assertEquals("\"****..d**...\"@example.org", censorUsername("\"john..doe...\"@example.org"));
    assertEquals("p.g@test.com", censorUsername("p.g@test.com"));
}
\$\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.