In recent years, I’ve been creating a calendar using images that I’ve taken. Back in 2002, I created a small Java program that prints out the calendar for the next twelve months. In my themes and on the MoodleBites eLearningWorld theme courses I have code that arranges the Moodle blocks horizontally, this is partly facilitated through the employment of column CSS classes that are based upon the ideas implemented in the Bootstrap framework. Combine all of these thoughts, and add to my recent posts with Java then I thought ‘Why can’t I get Java to create a calendar just like the one I have printed?’. And that’s what this month is all about, where we will additionally see how pre-processing of HTML output can be designed and implemented from scratch.
Disclaimers
Ubuntu® is a registered trademark of Canonical Ltd – ubuntu.com/legal/intellectual-property-policy . Firefox® is a registered trademark of the Mozilla Foundation. Moodle™ is a registered trademark of ‘Martin Dougiamas’ – moodle.com/trademarks . Other names / logos can be trademarks of their respective owners. Please review their website for details. I am independent from the organisations mentioned and am in no way writing for or endorsed by them. The information presented in this article is written according to my own understanding, there could be technical inaccuracies, so please do undertake your own research. The images used are my copyright, please don’t use outside of the context of this post / project without my permission.
References
- Bootstrap framework grid - getbootstrap.com/docs/5.3/layout/grid .
- Git - git-scm.com .
- Java 17 documentation - docs.oracle.com/en/java/javase/17/docs/api/index.html .
- MoodleBites theme courses - www.moodlebites.com/mod/page/view.php?id=3208 and www.moodlebites.com/mod/page/view.php?id=3210 .
- Mustache template - mustache.github.io .
- PHP - www.php.net .
Prerequisites
To understand and run the code presented, I recommend that you read my previous posts beforehand: A little bit of Java and A little bit more Java.
The calendar
Before we look at the code, which will take some time to read and understand, lets look at the web page output so that we have the image of the goal in mind. That’s the thing with software, when it gets complicated, having an understanding of its purpose keeps you going when it gets difficult:

The implementation
I have commented the code throughout to explain what each part does:
1/*
2 * CalGen.
3 *
4 * Generates the calendar for the year set, both as a HTML page from 'templated' and as text.
5 *
6 * Copyright (C) 2022 G J Barnard.
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 *
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
22 */
23
24import java.io.FileNotFoundException;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.util.Calendar;
28import java.util.Collections;
29import java.util.GregorianCalendar;
30import java.util.Iterator;
31import java.util.LinkedList;
32
33/**
34 * CalGen class.
35 *
36 * @copyright 2022 G J Barnard.
37 */
38public class CalGen {
39
40 // Calendar attributes.
41 private final GregorianCalendar gc = new GregorianCalendar(); // The calenadar.
42 private final LinkedList<Integer> months = new LinkedList<>(); // The days of the week.
43 private final LinkedList<Integer> days = new LinkedList<>(); // The days of the week.
44 private int currentMonth; // Keep track of the current month between methods.
45 private int previousMonth; // Keep track of the previous month between methods.
46 private final int theYear; // The year we are using.
47
48 // Template attributes.
49 private char[] calendarTemplate = null; // The template for the calendar.
50 private char[] monthTemplate = null; // The template of a month within the calendar.
51
52 private final char[] mpre = {'{', '{'}; // Template markup token start characters.
53 private final char[] mpost = {'}', '}'}; // Template markup token end characters.
54 private final FileOutputStream mout; // The stream for the markup html file.
55 // Stores the markup html as its being generated before it is output to the file.
56 private final StringBuffer markupOut = new StringBuffer();
57
58 /**
59 * Create the calendar and generate both the text and markup versions.
60 *
61 * @param args the command line arguments - not used.
62 * @throws java.io.FileNotFoundException If a template file cannot be found.
63 * @throws java.io.IOException If a problem occurs when reading a template file.
64 */
65 public static void main(String args[]) throws FileNotFoundException, IOException {
66 CalGen us = new CalGen();
67
68 us.calendar();
69 us.calendarTemplate();
70 }
71
72 /**
73 * Constructor.
74 *
75 * @throws FileNotFoundException If a template file cannot be found.
76 */
77 public CalGen() throws FileNotFoundException {
78 this.gc.setFirstDayOfWeek(Calendar.TUESDAY); // Change to another day if wished.
79 this.theYear = 2023;
80
81 // The name of the markup file in the current directory.
82 this.mout = new FileOutputStream("./" + this.theYear + "_Calendar.html");
83
84 // Add the months in the order we wish to output them as text.
85 months.add(Calendar.JANUARY);
86 months.add(Calendar.FEBRUARY);
87 months.add(Calendar.MARCH);
88 months.add(Calendar.APRIL);
89 months.add(Calendar.MAY);
90 months.add(Calendar.JUNE);
91 months.add(Calendar.JULY);
92 months.add(Calendar.AUGUST);
93 months.add(Calendar.SEPTEMBER);
94 months.add(Calendar.OCTOBER);
95 months.add(Calendar.NOVEMBER);
96 months.add(Calendar.DECEMBER);
97
98 // Add the days in the order used by default in the GregorianCalendar.
99 days.add(Calendar.SUNDAY);
100 days.add(Calendar.MONDAY);
101 days.add(Calendar.TUESDAY);
102 days.add(Calendar.WEDNESDAY);
103 days.add(Calendar.THURSDAY);
104 days.add(Calendar.FRIDAY);
105 days.add(Calendar.SATURDAY);
106
107 // Rotate the days around if needed so that the start day is first in the list.
108 Iterator<Integer> daysIt = days.iterator(); // The means of iterating over our list.
109 boolean found = false; // Have we found the day we are looking for?
110 int count = 0; // The number of positions the day we are looking for is away from Sunday.
111 Integer current; // The reference to the current day.
112 int firstDayOfWeek = this.gc.getFirstDayOfWeek(); // The day that the calendar has been set to be the first day of the week.
113
114 // While we have another day to check and we've not found the day we are looking for.
115 while (daysIt.hasNext() && found == false) {
116 current = daysIt.next(); // Get the next day.
117 if (current == firstDayOfWeek) { // Have we found the day we are looking for?
118 found = true; // Yes.
119 } else {
120 count++; // Increment the position.
121 }
122 }
123
124 if (count > 0) { // The day we are looking for is not Sunday, but is 'count' positions away from it.
125 // Rotate the list to the left by the number of positions we have calculated.
126 // The day we are looking for will then be the first.
127 Collections.rotate(days, -count);
128 }
129 }
130
131 /**
132 * Generate the text version of the calendar.
133 */
134 public void calendar() {
135 this.gc.set(theYear, 0, 1); // Set to the 1st January for the year we want.
136
137 // Output the year.
138 System.out.println(this.gc.get(Calendar.YEAR));
139
140 // Output the months.
141 Iterator<Integer> mit = this.months.iterator();
142 while (mit.hasNext()) {
143 this.month(mit.next());
144 System.out.println();
145 }
146 }
147
148 /**
149 * Output the month.
150 *
151 * @param theMonth The month to output.
152 */
153 private void month(int theMonth) {
154 this.gc.set(Calendar.MONTH, theMonth); // Tell the calendar the month we wish to use.
155 // Set both months to be the same so that we can detect when the current changes.
156 this.currentMonth = theMonth;
157 this.previousMonth = theMonth;
158
159 System.out.println(this.getMonthText(gc.get(Calendar.MONTH))); // Output the month text.
160
161 // Output the day names.
162 Iterator<Integer> daysIt = this.days.iterator();
163 Integer current;
164 while (daysIt.hasNext()) {
165 current = daysIt.next();
166 this.day(this.getDayText(current));
167 if (daysIt.hasNext()) {
168 System.out.print(" ");
169 } else {
170 System.out.println();
171 }
172 }
173
174 // Output the 'blank days' before the day on which the 1st of the month is.
175 // The current 'position' of the day in the week we are outputing, so '1' is the first day of the week.
176 int currentPosition = 1;
177 daysIt = this.days.iterator();
178 boolean startDayReached = false; // Have we found the start day?
179 int monthStartPostion = this.gc.get(Calendar.DAY_OF_WEEK); // Day of the week that the month starts on.
180
181 while (daysIt.hasNext() && (startDayReached == false)) {
182 current = daysIt.next();
183 if (current == monthStartPostion) {
184 startDayReached = true;
185 } else {
186 currentPosition++;
187 this.day("");
188 System.out.print(" ");
189 }
190 }
191
192 // Loop until we have reached the next month.
193 while (this.currentMonth == this.previousMonth) {
194
195 // Loop through the day 'positions' as we have outputted them with the day names.
196 while (currentPosition < 8) {
197
198 // Have we reached the next month?
199 if (this.currentMonth != this.previousMonth) {
200 // Are we on a week that has been started but not finished?
201 if (currentPosition != 1) {
202 // Loop through the remaining positions and output 'blank' days.
203 while (currentPosition < 8) {
204 this.day("");
205 System.out.print(" ");
206 currentPosition++;
207 }
208 }
209 } else {
210 // Output the day.
211 if (this.gc.get(Calendar.DAY_OF_MONTH) > 9) { // Get the prefixing spacing correct.
212 System.out.print(" ");
213 } else {
214 System.out.print(" ");
215 }
216 this.day(this.gc.get(Calendar.DAY_OF_MONTH)); // The day.
217 if (currentPosition < 7) {
218 System.out.print(" "); // Postfix space.
219 }
220
221 // Get the next day.
222 this.gc.add(Calendar.DAY_OF_MONTH, 1);
223 this.currentMonth = this.gc.get(Calendar.MONTH);
224
225 currentPosition++; // The next position in the week.
226 }
227 }
228 currentPosition = 1; // Reset to the next week.
229 System.out.println();
230 }
231 }
232
233 /**
234 * Output the day.
235 * @param day As an integer.
236 */
237 private void day(Integer day) {
238 this.day(day.toString());
239 }
240
241 /**
242 * Output the day.
243 *
244 * @param day As a string.
245 */
246 private void day(String day) {
247 System.out.print(day);
248 }
249
250 /**
251 * Generate the markup version of the calendar.
252 *
253 * @throws FileNotFoundException If a template file cannot be found.
254 * @throws IOException If a problem occurs when reading a template file.
255 */
256 public void calendarTemplate() throws FileNotFoundException, IOException {
257 this.gc.set(theYear, 0, 1); // Reset to the 1st January for the year we want.
258
259 this.loadTemplates(); // Load the templates.
260
261 // Process the calendar template.
262 int currentIndex = 0; // Index of the current character.
263 while (currentIndex < this.calendarTemplate.length) {
264 if ((this.calendarTemplate[currentIndex] == this.mpre[0]) &&
265 (this.calendarTemplate[currentIndex + 1]) == this.mpre[1]) {
266 // Start token.
267 currentIndex = currentIndex + 2; // Jump over the token start characters.
268 currentIndex = this.processCalendarToken(currentIndex); // Process the token.
269 } else {
270 // Pass through.
271 this.markupOut.append(this.calendarTemplate[currentIndex]); // Copy the character to the output.
272 currentIndex++; // Get the next character.
273 }
274 }
275
276 this.mout.write(this.markupOut.toString().getBytes()); // Write the markup to the output file.
277 this.mout.close(); // Close the file.
278 }
279
280 /**
281 * Load the templates.
282 *
283 * Ref: https://stackoverflow.com/questions/21980090/javas-randomaccessfile-eofexception
284 *
285 * @throws FileNotFoundException If a template file cannot be found.
286 * @throws IOException If a problem occurs when reading a template file.
287 */
288 private void loadTemplates() throws FileNotFoundException, IOException {
289 // Calendar template.
290 java.io.File templateFile = new java.io.File("CalendarTemplate.txt"); // Using the File object so that we can get the length.
291
292 char[] buffer = new char[(int) templateFile.length()]; // Buffer to store the read characters.
293
294 java.io.FileInputStream fin = new java.io.FileInputStream(templateFile); // Stream to read the file.
295 // Reader to read the file that is encoded with UTF-8 characters.
296 java.io.InputStreamReader isr = new java.io.InputStreamReader(fin, "UTF-8");
297
298 isr.read(buffer); // Read the file into the buffer.
299 isr.close(); // Close the file.
300
301 // To allow us to convert the bytes into characters.
302 StringBuilder sb = new StringBuilder((int) templateFile.length());
303 sb.append(buffer);
304
305 this.calendarTemplate = sb.toString().toCharArray(); // Convert into a string and then an array of chars.
306
307 // Month template.
308 // Same processing as the Calendar Template.
309 templateFile = new java.io.File("MonthTemplate.txt");
310 buffer = new char[(int) templateFile.length()];
311
312 fin = new java.io.FileInputStream(templateFile);
313 isr = new java.io.InputStreamReader(fin, "UTF-8");
314
315 isr.read(buffer);
316 isr.close();
317
318 sb = new StringBuilder((int) templateFile.length());
319 sb.append(buffer);
320
321 this.monthTemplate = sb.toString().toCharArray();
322 }
323
324 /**
325 * Process a token in the Calendar template.
326 *
327 * @param currentIndex The current character in the calendar template.
328 * @return The updated position in the template after processing the token so that we can continue.
329 */
330 private int processCalendarToken(int currentIndex) {
331 int end = this.calendarTemplate.length;
332 StringBuilder token = new StringBuilder();
333
334 // Extract the whole token until the end token characters are reached/
335 while (currentIndex < end) {
336 if ((this.calendarTemplate[currentIndex] == this.mpost[0]) &&
337 (this.calendarTemplate[currentIndex + 1]) == this.mpost[1]) {
338 // End token.
339 currentIndex = currentIndex + 2;
340 end = currentIndex; // Exit the loop.
341 } else {
342 // Characters of the token.
343 token.append(this.calendarTemplate[currentIndex]);
344 currentIndex++;
345 }
346 }
347 this.processCalendarToken(token.toString()); // Process the token.
348
349 return currentIndex;
350 }
351
352 /**
353 * Process the extracted token.
354 *
355 * @param token The token to process.
356 */
357 private void processCalendarToken(String token) {
358 int dataIndex = token.indexOf('-'); // Do we have 'parameter' data in the token?
359 String data = null;
360 String dataExtra = null;
361 if (dataIndex != -1) {
362 // We have data, so extract it.
363 data = token.substring(dataIndex + 1, token.length());
364 token = token.substring(0, dataIndex);
365
366 int ampIndex = data.indexOf('&'); // Do we have a second parameter data in the token?
367 if (ampIndex != -1) {
368 dataExtra = data.substring(ampIndex + 1, data.length());
369 data = data.substring(0, ampIndex);
370 }
371 }
372
373 // Identify and execute the action of the token with its data if any.
374 // Rule switch, Java 12 - https://blogs.oracle.com/javamagazine/post/new-switch-expressions-in-java-12
375 switch (token) {
376 case "calendartitle" -> this.markupOut.append(this.gc.get(Calendar.YEAR)).append(" Calendar");
377 case "jan" -> this.monthTemplate(Calendar.JANUARY, data, dataExtra);
378 case "feb" -> this.monthTemplate(Calendar.FEBRUARY, data, dataExtra);
379 case "mar" -> this.monthTemplate(Calendar.MARCH, data, dataExtra);
380 case "apr" -> this.monthTemplate(Calendar.APRIL, data, dataExtra);
381 case "may" -> this.monthTemplate(Calendar.MAY, data, dataExtra);
382 case "jun" -> this.monthTemplate(Calendar.JUNE, data, dataExtra);
383 case "jul" -> this.monthTemplate(Calendar.JULY, data, dataExtra);
384 case "aug" -> this.monthTemplate(Calendar.AUGUST, data, dataExtra);
385 case "sep" -> this.monthTemplate(Calendar.SEPTEMBER, data, dataExtra);
386 case "oct" -> this.monthTemplate(Calendar.OCTOBER, data, dataExtra);
387 case "nov" -> this.monthTemplate(Calendar.NOVEMBER, data, dataExtra);
388 case "dec" -> this.monthTemplate(Calendar.DECEMBER, data, dataExtra);
389 default -> this.markupOut.append("<p>Calendar error! Unknown token.</p>");
390 }
391 }
392
393 /**
394 * Execute a month token by processing the month template with the supplied parameters.
395 *
396 * @param theMonth The month.
397 * @param imageName The name of the image for the month.
398 * @param imageDescription The description of the image for the month.
399 */
400 private void monthTemplate(int theMonth, String imageName, String imageDescription) {
401 this.gc.set(Calendar.MONTH, theMonth); // Tell the calendar the month we want so that it tells us the correct days.
402 this.previousMonth = theMonth;
403 this.currentMonth = theMonth;
404
405 int currentIndex = 0; // Current character in the month template.
406 while (currentIndex < this.monthTemplate.length) {
407 if ((this.monthTemplate[currentIndex] == this.mpre[0]) && (this.monthTemplate[currentIndex + 1]) == this.mpre[1]) {
408 // Start token.
409 currentIndex = currentIndex + 2;
410 currentIndex = this.processMonthToken(currentIndex, imageName, imageDescription);
411 } else {
412 // Pass through.
413 this.markupOut.append(this.monthTemplate[currentIndex]);
414 currentIndex++;
415 }
416 }
417 }
418
419 /**
420 * Execute a month token.
421 *
422 * @param currentIndex The current character in the month template.
423 * @param imageName The name of the image for the month.
424 * @param imageDescription The description of the image for the month.
425 */
426 private int processMonthToken(int currentIndex, String imageName, String imageDescription) {
427 int end = this.monthTemplate.length;
428 StringBuilder token = new StringBuilder();
429 while (currentIndex < end) {
430 if ((this.monthTemplate[currentIndex] == this.mpost[0]) && (this.monthTemplate[currentIndex + 1]) == this.mpost[1]) {
431 // End token.
432 currentIndex = currentIndex + 2;
433 end = currentIndex; // Exit the loop.
434 } else {
435 // Characters of the token.
436 token.append(this.monthTemplate[currentIndex]);
437 currentIndex++;
438 }
439 }
440 this.processMonthToken(token.toString(), imageName, imageDescription);
441
442 return currentIndex;
443 }
444
445 /**
446 * Process a month token.
447 *
448 * @param token The month token.
449 * @param imageName The name of the image for the month.
450 * @param imageDescription The description of the image for the month.
451 */
452 private void processMonthToken(String token, String imageName, String imageDescription) {
453 int dataIndex = token.indexOf('-');
454 String data = null;
455 if (dataIndex != -1) {
456 // We have data.
457 data = token.substring(dataIndex + 1, token.length());
458 token = token.substring(0, dataIndex);
459 }
460
461 switch (token) {
462 case "monthtitle" -> this.markupOut.append(this.getMonthText(gc.get(Calendar.MONTH)));
463 case "monthdaynames" -> this.monthDayNames(data);
464 case "monthweek" -> this.monthWeek(data);
465 case "monthimage" -> this.monthImage(imageName);
466 case "monthimagedescription" -> this.monthImage(imageDescription);
467 default -> this.markupOut.append("<p>Month error! Unknown token.</p>");
468 }
469 }
470
471 /**
472 * Put the month day in the markup output.
473 *
474 * @param data The token parameter, which is the wrapper html around the day text.
475 */
476 private void monthDayNames(String data) {
477 int starIndex = data.indexOf('*'); // Where the day text should be placed in the wrapper markup.
478 String pre = data.substring(0, starIndex); // Wrapper opening tag.
479 String post = data.substring(starIndex + 1, data.length()); // Wrapper closing tag.
480
481 // Output the days of the week in the order that we have set.
482 Iterator<Integer> daysIt = this.days.iterator();
483 Integer current;
484 while (daysIt.hasNext()) {
485 current = daysIt.next();
486 this.monthDay(this.getDayText(current), pre, post);
487 }
488 }
489
490 /**
491 * Output a day as an integer wrapped within the pre and post wrapper markup, the opening / closing tags.
492 *
493 * @param day The day.
494 * @param pre The opening wrapper tag.
495 * @param post The closing wrapper tag.
496 */
497 private void monthDay(Integer day, String pre, String post) {
498 this.monthDay(day.toString(), pre, post);
499 }
500
501 /**
502 * Output a day as text wrapped within the pre and post wrapper markup, the opening / closing tags.
503 *
504 * @param day The day.
505 * @param pre The opening wrapper tag.
506 * @param post The closing wrapper tag.
507 */
508 private void monthDay(String day, String pre, String post) {
509 this.markupOut.append(pre).append(day).append(post);
510 }
511
512 /**
513 * Output the days in the month as weeks.
514 *
515 * @param data The week and day wrapper markup
516 */
517 private void monthWeek(String data) {
518 int exlamationIndex = data.indexOf('!'); // The position of the week within its wrapper markup.
519 int ampIndex = data.indexOf('&'); // The day parameter wrapper markup.
520 int starIndex = data.indexOf('*'); // The position of the day within its wrapper markup.
521
522 String weekPre = data.substring(0, exlamationIndex);
523 String weekPost = data.substring(exlamationIndex + 1, ampIndex);
524
525 String dayPre = data.substring(ampIndex + 1, starIndex);
526 String dayPost = data.substring(starIndex + 1, data.length());
527
528 int currentPosition = 1;
529 boolean startDayReached = false;
530
531 // Similar logic / structure to that of outputting the text, but this time we have to output the 'blank' days within the
532 // loop so that it is withing the wrapper markup for the week.
533 while (this.currentMonth == this.previousMonth) {
534 this.markupOut.append(weekPre);
535
536 if (startDayReached == false) {
537 Iterator<Integer> daysIt = this.days.iterator();
538 int monthStartPostion = this.gc.get(Calendar.DAY_OF_WEEK); // Day of the week that the month starts on.
539 Integer current;
540
541 while (daysIt.hasNext() && (startDayReached == false)) {
542 current = daysIt.next();
543 if (current == monthStartPostion) {
544 startDayReached = true;
545 } else {
546 currentPosition++;
547 this.monthDay("", dayPre, dayPost);
548 }
549 }
550 }
551
552 while (currentPosition < 8) {
553 if (this.currentMonth != this.previousMonth) {
554 if (currentPosition != 1) {
555 while (currentPosition < 8) {
556 this.monthDay("", dayPre, dayPost);
557 currentPosition++;
558 }
559 }
560 } else {
561 this.monthDay(this.gc.get(Calendar.DAY_OF_MONTH), dayPre, dayPost);
562 this.gc.add(Calendar.DAY_OF_MONTH, 1);
563 this.currentMonth = this.gc.get(Calendar.MONTH);
564
565 currentPosition++;
566 }
567 }
568 currentPosition = 1;
569
570 this.markupOut.append(weekPost);
571 }
572 }
573
574 /**
575 * Output the image name / description in the place of its token, no wrapper here.
576 *
577 * @param text The text to use, if 'null' then don't output otherwise "null" will appear in the markup!
578 */
579 private void monthImage(String text) {
580 if (text != null) {
581 this.markupOut.append(text);
582 }
583 }
584
585 /**
586 * Given the month as a number return its string representation.
587 *
588 * @param theMonth The month.
589 * @return The name of the month.
590 */
591 private String getMonthText(int theMonth) {
592 return switch (theMonth) {
593 case Calendar.JANUARY -> "January";
594 case Calendar.FEBRUARY -> "February";
595 case Calendar.MARCH -> "March";
596 case Calendar.APRIL -> "April";
597 case Calendar.MAY -> "May";
598 case Calendar.JUNE -> "June";
599 case Calendar.JULY -> "July";
600 case Calendar.AUGUST -> "August";
601 case Calendar.SEPTEMBER -> "September";
602 case Calendar.OCTOBER -> "October";
603 case Calendar.NOVEMBER -> "November";
604 case Calendar.DECEMBER -> "December";
605 default -> "Unknown";
606 };
607 }
608
609 /**
610 * Given the day as a number return its string representation.
611 *
612 * @param theDay The day.
613 * @return The name of the day.
614 */
615 private String getDayText (int theDay) {
616 return switch (theDay) {
617 case Calendar.SUNDAY -> "Sun";
618 case Calendar.MONDAY -> "Mon";
619 case Calendar.TUESDAY -> "Tue";
620 case Calendar.WEDNESDAY -> "Wed";
621 case Calendar.THURSDAY -> "Thu";
622 case Calendar.FRIDAY -> "Fri";
623 case Calendar.SATURDAY -> "Sat";
624 default -> "Unknown";
625 };
626 }
627}
What exactly is going on? Well, firstly there is the textual output of the calendar. Its purpose is to show that the logic is working, a means of developing the more complex HTML markup output as it evolved from the original code, and can be copied and used in other documents:

Secondly is the creation of the HTML markup file that is rendered by a web browser. This employs two custom ’template’ files that represent the calendar and the months within. The syntax of the ’tokens’ that the code replaces with data is based upon ‘Mustache’ but is bespoke for this project. Like Mustache and indeed PHP, but not so complex, we are pre-processing HTML which contains our own syntax into a form that is completely HTML that the web browser understands.
If we look at the first template, ‘CalendarTemplate.txt’:
1<!doctype html>
2<html>
3 <head>
4 <style type="text/css">
5 .cal-row {
6 display: flex;
7 flex-wrap: wrap;
8 justify-content:
9 space-evenly; margin: 10px;
10 }
11
12 .cal-row.nowrap {
13 flex-wrap: nowrap;
14 }
15
16 .cal-1 {
17 flex-basis: 100%;
18 }
19
20 .cal-4 {
21 flex-basis: 25%;
22 }
23
24 .cal-7 {
25 flex-basis: 14.28%;
26 margin-left: 2px;
27 margin-right: 2px;
28 }
29
30 .cal-7:first-child {
31 margin-left: 0;
32 }
33
34 .cal-7:first-child {
35 margin-right: 0;
36 }
37
38 .cal-img {
39 height: auto;
40 max-width: 100%;
41 }
42
43 .monthdayname,
44 .monthday {
45 text-align: end;
46 }
47
48 h1,
49 .monthtitle {
50 text-align: center;
51 }
52
53 * {
54 font-family: sans-serif;
55 }
56 </style>
57 <title>{{calendartitle}}</title>
58 </head>
59 <body>
60 <h1>{{calendartitle}}</h1>
61 <div class="cal-row">
62 {{jan-Jan_760D_3389_sRGB.webp&Female mallard duck on ice}}
63 {{feb-Feb_760D_0509_sRGB.webp&Pigeon looking at the camera standing on a fence}}
64 {{mar-Mar_760D_5005_sRGB.webp&Cygnet in the sun}}
65 {{apr-Apr_760D_8255_sRGB.webp&Mallard duckling}}
66 {{may-May_760D_4222_sRGB.webp&Midland Pullman Train, Class 43 version}}
67 {{jun-Jun_760D_4905_sRGB.webp&Beer beach, Devon}}
68 {{jul-Jul_760D_4809_sRGB.webp&Moorhen}}
69 {{aug-Aug_760D_6863_sRGB.webp&Male mallard duck}}
70 {{sep-Sep_760D_4164_sRGB.webp&Great Western Railway Class 43, 43198, Driver Stan Martin 25th June 1950 - 6th November 2004}}
71 {{oct-Oct_760D_0803_sRGB.webp&White swan looking at its reflection in the water}}
72 {{nov-Nov_760D_3724_sRGB.webp&Chaffinch on a fence}}
73 {{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}
74 </div>
75 </body>
76</html>
Then most of it is HTML that we are familiar with, until we come to a token. Like Mustache, I have employed the curly brackets ‘{{’ and ‘}}’ as opening and closing indicators. Between the indicators is the token itself, which depending on which one it is can have one or two parameters.
If we look at ‘<title>{{calendartitle}}</title>’, then the markup will be sent to the output but the ‘{{calendartitle}}’ token will be replaced by the actual title of the calendar by the method ‘processCalendarToken’. Looking at ‘{{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}’, then that is a month token that says to output December with the image file ‘Dec_760D_3366_sRGB.webp’ which has a description of ‘Robin looking skywards’ that will go in the ‘img’ tag ‘alt’ attribute for accessibility. This we can see in the ‘MonthTemplate.txt’:
1<div class="cal-4 month">
2 <div class="cal-row nowrap monthheader">
3 <div class="cal-1 monthtitle">{{monthtitle}}</div>
4 </div>
5 <div class="cal-row monthimagewrapper">
6 <img class="cal-1 cal-img monthimage" src="{{monthimage}}" alt="{{monthimagedescription}}">
7 </div>
8 <div class="cal-row nowrap monthdaynames">
9 {{monthdaynames-<div class="cal-7 monthdayname">*</div>}}
10 </div>
11 {{monthweek-<div class="cal-row nowrap monthweek">!</div>&<div class="cal-7 monthday">*</div>}}
12</div>
We need to use this template as the month, just like the day, is repeated. But unlike the day, the output is a combination of lots of data that has its own wrapper markup. The processing of this template works in the same way as the calendar template.
Running
Get the code by retrieving the entire contents of the folder ‘
CalGen
’ or if you’re familiar with ‘Git’, then use ‘git clone https://github.com/gjb2048/code.git’ to clone my ‘code’ repository then go to the CalGen folder and run ‘javac CalGen.java’ to create the ‘class’ file, followed by ‘java CalGen’ to create the calendar, which all being well should produce the text version to the console:

And we’ll have a new file in the folder called ‘2023_Calendar.html’:

If your machine has a web browser installed or is running a web server, then open / copy the it (and the images) to where they can be served. I’m using a headless Raspberry Pi in this example, so I need to copy ‘2023_Calendar.html’ and the webp images to the Ubuntu virtual machine I’m using:

Then we can open it in a web browser:

The code has not been written in a way that it is robust to user errors and as such if you modify the template files to use your own images and / or change the structure of the page then you need to be sure that you’ve done this correctly. Such as having the first day of the week to be Friday ‘this.gc.setFirstDayOfWeek(Calendar.FRIDAY);’ and the months to be ordered in columns and not rows:
1<div class="cal-row">
2 {{jan-Jan_760D_3389_sRGB.webp&Female mallard duck on ice}}
3 {{apr-Apr_760D_8255_sRGB.webp&Mallard duckling}}
4 {{jul-Jul_760D_4809_sRGB.webp&Moorhen}}
5 {{oct-Oct_760D_0803_sRGB.webp&White swan looking at its reflection in the water}}
6 {{feb-Feb_760D_0509_sRGB.webp&Pigeon looking at the camera standing on a fence}}
7 {{may-May_760D_4222_sRGB.webp&Midland Pullman Train, Class 43 version}}
8 {{aug-Aug_760D_6863_sRGB.webp&Male mallard duck}}
9 {{nov-Nov_760D_3724_sRGB.webp&Chaffinch on a fence}}
10 {{mar-Mar_760D_5005_sRGB.webp&Cygnet in the sun}}
11 {{jun-Jun_760D_4905_sRGB.webp&Beer beach, Devon}}
12 {{sep-Sep_760D_4164_sRGB.webp&Great Western Railway Class 43, 43198, Driver Stan Martin 25th June 1950 - 6th November 2004}}
13 {{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}
14</div>
giving:

But the output of the text will be the same as it’s month order has not been changed. How do you think the code could be modified to achieve this?
A few extra thoughts
I’ve written the code in such a way that there is only one place to change for each concept, i.e. only one place to state what day of the week the month starts on.
Development can take time and hit the odd brick wall, such as making the code work with any start day. There is a flow to the development, where I’ve had an intermediate step of generating the markup within the code with no tokens and adding the data in place. If you want to see all of the stages, then do look at the history of the code: ‘ github.com/gjb2048/code/commits/main/Java/CalGen/CalGen.java ’ and ‘ github.com/gjb2048/code/commits/456dbcd937349fc094357e66a64b569dd9ec119c/Java/CalGen/src/CalGen.java?browsing_rename_history=true&new_path=Java/CalGen/CalGen.java&original_branch=main ’.
Don’t give up when hitting a brick wall during development but ask yourself ‘Why does this not work?’ and walk away from the screen for a while. With the start day issue, I discovered that the array index calculations weren’t flexible enough to cope with multiple day differences. Perhaps its because I’m not that mathematical. After a pause, I thought about the problem again and realised that I needed to get the days of the week in the order I wanted first, with their associated index. Then the code at the start of each month, iterate from the first day of the week to the start day, because now it was working with a list it could traverse regardless of the index number (was being used for the previous array data type). And when the end of the month occurred, then just fill in the blanks for the remaining days.
Conclusion
In this post, we’ve moved on from my previous Java posts to introduce the use of additional files, both input and output, whilst building upon the concept of streams.
I hope you can now envisage in a different way how the concept of taking a static template, parsing it and adding data from a different source can be achieved from the ground up. This concept is essentially how Moodle works using PHP and JavaScript to generate the output (web page) from the static files and database data it has.
Do have a go.