CompSci.rocks
FRQcsapa

LogMessage / SystemLog: 2016 FRQ 2

A step-by-step solution to the 2016 AP CSA FRQ 2 (LogMessage / SystemLog), covering parsing a String with indexOf and substring, then filtering matching entries out of a List<LogMessage> in Java.

Server logs are just text with a fixed shape hiding inside them, and this AP Computer Science A free-response question asks you to pull that structure apart by hand — first splitting a single message into its two pieces, then sifting a whole list of messages down to just the ones that match a keyword.

What This FRQ Tests

  • AP CSA units: Unit 5 (Writing Classes), Unit 3/4 (Boolean logic and iteration for String parsing), and Unit 7 (ArrayList/List)
  • Core skill: locating a delimiter inside a String and splitting around it with indexOf/substring
  • Secondary skill: removing matching elements from a List while walking through it, without accidentally skipping any
  • Official category: this year's version doesn't sit neatly in one modern category. Parts (a)–(b) are Classes work (Unit 5) — completing the LogMessage constructor and writing one of its instance methods. Part (c) is Array/ArrayList work (Unit 7) — filtering entries out of a SystemLog's List<LogMessage>. It's printed as FRQ 2 in the 2016 booklet; FRQ 1 that same year (RandomStringChooser) was also Classes-flavored, which is a clear sign 2016 predates the fixed one-category-per-slot ordering that wasn't formalized until the 2019–2020 course redesign.

The Setup

  • A LogMessage object has two private fields: machineId and description.
  • Every valid log message has the exact format machineId:description — exactly one colon, with no spaces immediately before or after it.
  • A description properly contains a keyword only if all three hold: the keyword is a substring of the description; the keyword is at the very start of the description, or immediately preceded by a space; and the keyword is at the very end, or immediately followed by a space.
  • A SystemLog holds private List<LogMessage> messageList (guaranteed non-null, with only non-null entries).
  • removeMessages(keyword) removes every entry whose description properly contains keyword, returning those removed entries as a new list — both the returned list and whatever's left in messageList must keep their original relative order.
  • Three things to write: the LogMessage constructor (part a), containsWord (part b), and SystemLog's removeMessages (part c).

Part (a): Writing the LogMessage Constructor

The Rule, Broken Down

  1. Every valid message has exactly one colon — no more, no fewer.
  2. Everything before that colon becomes machineId.
  3. Everything after it becomes description.

Step-by-Step Approach

  1. Find the colon's position with indexOf(":").
  2. Everything from index 0 up to (but not including) the colon is machineId.
  3. Everything after the colon, to the end of the string, is description.

The Code

public LogMessage(String message)
{
    int colonIndex = message.indexOf(":");
    machineId = message.substring(0, colonIndex);
    description = message.substring(colonIndex + 1);
}

Why Each Piece Matters

  • indexOf(":") finds the one guaranteed colon — since the precondition guarantees exactly one, there's no ambiguity about which one it is.
  • substring(0, colonIndex) uses the two-argument form, which stops before the given end index, so the colon itself is never included in machineId.
  • substring(colonIndex + 1) uses the one-argument form — "from this index to the end of the string" — and the + 1 is what skips over the colon character itself, landing exactly on the first character of description.

Common Mistakes to Avoid

  • Forgetting the + 1 when computing description, which would leave the colon as the first character of the description.
  • Assuming a fixed split position (like "the first 6 characters are always the machine ID") instead of locating the colon — machine IDs vary in length ("Webserver" vs. "SERVER1" vs. "CLIENT3").
  • Using substring(0, colonIndex + 1) for machineId — that extra + 1 would tack the colon onto the end of the machine ID instead of excluding it.

Part (b): Writing containsWord

The Rule, Broken Down

The three "properly contains" conditions all boil down to one idea: the keyword has to be surrounded by word boundaries — either the edge of the string, or a space — on both sides.

Step-by-Step Approach

  1. Add a single space to the front and the back of description. This turns "the very start" and "the very end" of the string into ordinary space characters.
  2. Build the exact substring being searched for: a space, then the keyword, then a space.
  3. Search the padded description for that padded keyword with indexOf.

The Code

public boolean containsWord(String keyword)
{
    String paddedDescription = " " + description + " ";
    String paddedKeyword = " " + keyword + " ";

    return paddedDescription.indexOf(paddedKeyword) != -1;
}

Why Each Piece Matters

  • Padding both strings with spaces converts all three original conditions into a single, uniform check: "does " keyword " appear anywhere in " description "?" A keyword at the very start of description now has a real space in front of it (from the padding), and the same goes for the very end.
  • indexOf returns -1 when nothing is found — comparing against != -1 turns that directly into the true/false this method needs to return.
  • No manual index math or bounds-checking is needed at all — the padding trick does all of that work up front, before the search even happens.

Tracing the Example

Using the question's own two tables of descriptions checked against the keyword "disk":

Description Padded description Contains " disk "? Properly contains "disk"?
"disk" " disk " yes true
"error on disk" " error on disk " yes true
"error on /dev/disk disk" " error on /dev/disk disk " yes (the second disk) true
"error on disk DSK1" " error on disk DSK1 " yes true
"DISK" " DISK " no (wrong case) false
"error on disk3" " error on disk3 " no (followed by 3, not a space) false
"error on /dev/disk" " error on /dev/disk " no (preceded by /, not a space) false
"diskette" " diskette " no (followed by ette, not a space) false

Every one of the eight examples from the question matches exactly.

Common Mistakes to Avoid

  • Checking only description.indexOf(keyword) != -1 with no boundary logic at all — that would incorrectly mark "diskette" and "error on disk3" as properly containing "disk".
  • Lowercasing everything before comparing — the examples make clear that "DISK" should not match "disk"; case matters here, and String.equals/indexOf are already case-sensitive by default, so nothing extra needs to be done.
  • Padding only one side (front or back, not both) — that reintroduces exactly the edge case the padding trick was meant to eliminate.

Part (c): Writing removeMessages

The Rule, Broken Down

  1. Every entry in messageList whose description properly contains keyword must be removed from messageList and collected into a new list.
  2. The rubric requires calling containsWord — reimplementing that matching logic here loses credit.
  3. Both the returned list and whatever remains in messageList must preserve their original relative order.

Step-by-Step Approach

  1. Create a new, empty List<LogMessage> to collect the removed entries.
  2. Walk through messageList using an index — but don't always advance it.
  3. At each index: if that entry's description properly contains keyword, remove it from messageList and add the removed entry to the results list. Don't advance the index this iteration, because the next entry has just shifted into the spot that was vacated.
  4. If it doesn't match, advance the index instead.
  5. Stop once the index reaches the (shrinking) size of messageList.
  6. Return the results list.

The Code

public List<LogMessage> removeMessages(String keyword)
{
    List<LogMessage> removed = new ArrayList<LogMessage>();
    int i = 0;

    while (i < messageList.size())
    {
        if (messageList.get(i).containsWord(keyword))
        {
            removed.add(messageList.remove(i));
        }
        else
        {
            i++;
        }
    }

    return removed;
}

Why Each Piece Matters

  • messageList.remove(i) shifts every later element one position to the left, per ArrayList's own documented behavior — so re-checking the same index i on the next loop iteration is correct, not a bug, since a new entry has slid into that exact spot.
  • i only advances inside the else branch — that's what guarantees every entry gets checked exactly once, with nothing skipped, no matter how many entries in a row get removed.
  • Calling containsWord instead of rewriting keyword-matching logic is both required by the problem and avoids two separate (and possibly inconsistent) implementations of the same rule.
  • removed.add(messageList.remove(i)) relies on remove(int index) doing two things at once — deleting the element and returning it — the exact same pattern the RandomStringChooser FRQ used for getNext().

Tracing the Example

Using the question's own six-message log and keyword = "disk":

Step Index checked Entry containsWord("disk")? Action Index after
1 0 CLIENT3:security alert... false advance 1
2 1 Webserver:disk offline true remove, add to results 1 (unchanged)
3 1 SERVER1:file not found (shifted in) false advance 2
4 2 SERVER2:read error on disk DSK1 true remove, add to results 2 (unchanged)
5 2 SERVER1:write error on disk DSK2 (shifted in) true remove, add to results 2 (unchanged)
6 2 Webserver:error on /dev/disk (shifted in) false advance 3

The loop ends once the index (3) reaches the shrunken list's size (3). The returned list — Webserver:disk offline, SERVER2:read error on disk DSK1, SERVER1:write error on disk DSK2 — matches the question's expected output exactly, and the three entries left behind in messageListCLIENT3:..., SERVER1:file not found, Webserver:error on /dev/disk — match the question's "after the call" list exactly too, in their original order.

Common Mistakes to Avoid

  • Always incrementing the index, even after a removal. This is the classic ArrayList-removal-while-iterating bug — it skips checking whatever entry just shifted into the vacated spot.
  • Iterating with a for-each loop while removing from the same list — this throws a ConcurrentModificationException in real Java, since the list is being structurally changed mid-iteration.
  • Reimplementing "properly contains" logic instead of calling containsWord — the rubric specifically requires using it, and it also avoids maintaining the same rule in two places.
  • Building the removed list out of order, e.g. by scanning backward — the postcondition explicitly requires the returned list to preserve the original order the entries appeared in.

Key Takeaways

  • When a rule is "at the start OR preceded by a space" combined with "at the end OR followed by a space," padding both the text being searched and the thing being searched for with a boundary character often collapses several edge cases into one ordinary substring search.
  • Removing elements from a List while iterating forward only requires advancing the index when nothing was removed — re-checking the same index right after a removal is correct, not redundant, because everything after it just shifted down.
  • A class that holds a private List of another class's objects (SystemLog holding LogMessage) is a common AP CSA combination — "Classes" and "Array/ArrayList" skills aren't mutually exclusive categories, and a single FRQ can lean on both.

Related FRQs