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
Stringand splitting around it withindexOf/substring - Secondary skill: removing matching elements from a
Listwhile 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
LogMessageconstructor and writing one of its instance methods. Part (c) is Array/ArrayList work (Unit 7) — filtering entries out of aSystemLog'sList<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
LogMessageobject has two private fields:machineIdanddescription. - 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
SystemLogholdsprivate List<LogMessage> messageList(guaranteed non-null, with only non-nullentries). removeMessages(keyword)removes every entry whose description properly containskeyword, returning those removed entries as a new list — both the returned list and whatever's left inmessageListmust keep their original relative order.- Three things to write: the
LogMessageconstructor (part a),containsWord(part b), andSystemLog'sremoveMessages(part c).
Part (a): Writing the LogMessage Constructor
The Rule, Broken Down
- Every valid
messagehas exactly one colon — no more, no fewer. - Everything before that colon becomes
machineId. - Everything after it becomes
description.
Step-by-Step Approach
- Find the colon's position with
indexOf(":"). - Everything from index
0up to (but not including) the colon ismachineId. - 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 inmachineId.substring(colonIndex + 1)uses the one-argument form — "from this index to the end of the string" — and the+ 1is what skips over the colon character itself, landing exactly on the first character ofdescription.
Common Mistakes to Avoid
- Forgetting the
+ 1when computingdescription, 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)formachineId— that extra+ 1would 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
- 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. - Build the exact substring being searched for: a space, then the keyword, then a space.
- 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 ofdescriptionnow has a real space in front of it (from the padding), and the same goes for the very end. indexOfreturns-1when nothing is found — comparing against!= -1turns that directly into thetrue/falsethis 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) != -1with 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, andString.equals/indexOfare 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
- Every entry in
messageListwhose description properly containskeywordmust be removed frommessageListand collected into a new list. - The rubric requires calling
containsWord— reimplementing that matching logic here loses credit. - Both the returned list and whatever remains in
messageListmust preserve their original relative order.
Step-by-Step Approach
- Create a new, empty
List<LogMessage>to collect the removed entries. - Walk through
messageListusing an index — but don't always advance it. - At each index: if that entry's description properly contains
keyword, remove it frommessageListand 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. - If it doesn't match, advance the index instead.
- Stop once the index reaches the (shrinking) size of
messageList. - 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, perArrayList's own documented behavior — so re-checking the same indexion the next loop iteration is correct, not a bug, since a new entry has slid into that exact spot.ionly advances inside theelsebranch — that's what guarantees every entry gets checked exactly once, with nothing skipped, no matter how many entries in a row get removed.- Calling
containsWordinstead 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 onremove(int index)doing two things at once — deleting the element and returning it — the exact same pattern theRandomStringChooserFRQ used forgetNext().
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 messageList — CLIENT3:..., 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
ConcurrentModificationExceptionin 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
Listwhile 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
Listof another class's objects (SystemLogholdingLogMessage) is a common AP CSA combination — "Classes" and "Array/ArrayList" skills aren't mutually exclusive categories, and a single FRQ can lean on both.