[{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/aws/","section":"Tags","summary":"","title":"Aws","type":"tags"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/chatgpt/","section":"Tags","summary":"","title":"Chatgpt","type":"tags"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/authors/christopher-taylor/","section":"Authors","summary":"","title":"Christopher Taylor","type":"authors"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/cloudfront/","section":"Tags","summary":"","title":"Cloudfront","type":"tags"},{"content":"Welcome to cstaylor.org, a home for essays, technical notes, and project updates.\n","date":"July 11, 2026","externalUrl":null,"permalink":"/","section":"cstaylor.org","summary":"Welcome to cstaylor.org, a home for essays, technical notes, and project updates.\n","title":"cstaylor.org","type":"page"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"Github-Actions","type":"tags"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"Browse the latest posts from www.cstaylor.org.\n","date":"July 11, 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"Browse the latest posts from www.cstaylor.org.\n","title":"Posts","type":"posts"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/categories/site/","section":"Categories","summary":"","title":"Site","type":"categories"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"July 11, 2026","externalUrl":null,"permalink":"/tags/terraform/","section":"Tags","summary":"","title":"Terraform","type":"tags"},{"content":"This site started life as a WordPress blog. It eventually moved onto an EC2 instance, where I was paying for and maintaining a server to deliver a collection of pages that rarely changed. That worked, but it was more infrastructure than a static blog really needed.\nI used ChatGPT 5.6 to help convert the site to Hugo using the Blowfish theme, and then to replace the EC2 hosting with a private Amazon S3 bucket behind Amazon CloudFront.\nThe final architecture is pleasantly small:\n1Markdown content -\u0026gt; Hugo -\u0026gt; Amazon S3 -\u0026gt; CloudFront -\u0026gt; www.cstaylor.org Building the static site # The old posts are now Markdown files in the Git repository. Hugo turns them into a static site, so there is no database, PHP runtime, WordPress installation, or application server to patch.\nChatGPT helped create the Hugo configuration, install Blowfish as a Hugo module, organize the old content, and verify that important routes still rendered correctly. The production build is a single command:\n1hugo --gc --minify Replacing EC2 with S3 and CloudFront # The generated site is uploaded to a versioned S3 bucket. The bucket blocks all public access; CloudFront is the only service allowed to read it, using an Origin Access Control and signed requests.\nCloudFront provides the public endpoint, TLS certificate, compression, caching, HTTP-to-HTTPS redirects, and custom error responses. A small CloudFront Function translates Hugo\u0026rsquo;s clean URLs, such as /about/, into the corresponding S3 object, /about/index.html.\nThe AWS infrastructure is described with Terraform rather than being a collection of console settings. That includes the bucket, CloudFront distribution, certificate, access policy, clean-URL function, and deployment identity.\nPublishing through GitHub Actions # Publishing is now automatic. When new content is committed to the main branch, GitHub Actions:\nChecks out the repository. Installs the pinned Hugo version. Builds the site. Assumes a narrowly scoped AWS role using OpenID Connect. Synchronizes the generated files to S3. Invalidates the CloudFront cache. The OpenID Connect setup is an important detail. GitHub receives temporary AWS credentials for each deployment, so there are no long-lived AWS access keys stored in GitHub secrets. The role can only update this site\u0026rsquo;s bucket and invalidate this site\u0026rsquo;s CloudFront distribution.\nThe result # The site is now served globally through CloudFront without a continuously running web server. Direct access to the S3 origin is blocked, HTTPS is automatic, infrastructure changes are reproducible, and publishing a post is just a Git commit.\nChatGPT did more than produce a checklist. It inspected the existing repository, wrote and validated the Terraform configuration, provisioned the AWS resources, migrated DNS in safe stages, tested the live routes, added the deployment workflow, and monitored the first automated release.\nFor a small, mostly static blog, this feels like a much better fit than keeping an EC2 instance alive indefinitely.\nAlso, this blog entry was written by ChatGPT, summarizing the work we did together during this migration. I\u0026rsquo;m very impressed with the results.\n","date":"July 11, 2026","externalUrl":null,"permalink":"/posts/using-chatgpt-5-6-to-convert-my-old-wordpress-blog-to-cloudfront/","section":"Posts","summary":"Moving an old WordPress blog from EC2 to a static Hugo site served by S3 and CloudFront.","title":"Using ChatGPT 5.6 to convert my old WordPress blog to CloudFront","type":"posts"},{"content":"A collection of interesting things I\u0026rsquo;ve seen, as I\u0026rsquo;ve seen them. Hopefully what I describe will be useful for you.\nThis site is built with Hugo and the Blowfish theme.\n","date":"July 9, 2026","externalUrl":null,"permalink":"/about/","section":"cstaylor.org","summary":"A collection of interesting things I’ve seen, as I’ve seen them. Hopefully what I describe will be useful for you.\nThis site is built with Hugo and the Blowfish theme.\n","title":"About","type":"page"},{"content":"","date":"December 12, 2014","externalUrl":null,"permalink":"/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"If you look at the AbstractSequentialIterator class in Google Guava, you’ll see that computeNext is called in the finally block of the next method.\nWhat does that mean? You cannot reuse the previous object nor alter its value in the computeNext method.\n","date":"December 12, 2014","externalUrl":null,"permalink":"/posts/interesting-behavior-with-guava-asi/","section":"Posts","summary":"A helpful reminder about ASI","title":"Interesting behavior with Guava \u003ccode\u003eAbstractSequentialIterator\u003c/code\u003e and reusing objects","type":"posts"},{"content":"","date":"November 29, 2014","externalUrl":null,"permalink":"/categories/development/","section":"Categories","summary":"","title":"Development","type":"categories"},{"content":"","date":"November 29, 2014","externalUrl":null,"permalink":"/tags/guava/","section":"Tags","summary":"","title":"Guava","type":"tags"},{"content":"","date":"November 29, 2014","externalUrl":null,"permalink":"/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"Guava caches are great, but developing with them can be troublesome.\nOne solution is wrapping the underlying cache using a simple implementation of the ForwardingCache class:\n1public class DevelopmentForwardingCache\u0026lt;K,V\u0026gt; extends ForwardingCache\u0026lt;K,V\u0026gt; 2{ 3 private Cache\u0026lt;K,V\u0026gt; delegate; 4 5 private boolean debug; 6 7 public DevelopmentForwardingCache ( Cache\u0026lt;K,V\u0026gt; delegate ) 8 { 9 this.delegate = delegate; 10 } 11 12 public DevelopmentForwardingCache\u0026lt;K,V\u0026gt; DEBUG ( ) 13 { 14 this.debug = true; 15 return this; 16 } 17 18 @Override 19 protected Cache\u0026lt;K, V\u0026gt; delegate ( ) 20 { 21 return this.delegate; 22 } 23 24 @Override 25 public V get ( K key, Callable\u0026lt;? extends V\u0026gt; valueLoader ) throws ExecutionException 26 { 27 V ret_val = super.get(key, valueLoader); 28 if ( debug ) invalidate(key); 29 return ret_val; 30 } 31} Instead of using a Cache directly from a CacheBuilder, you can wrap it with this class and toggle debug mode.\nNote: if you’re using a LoadingCache, you’ll want to derive from the ForwardingLoadingCache instead and override the get(K key) and getAll(Iterable\u0026lt;? extends K\u0026gt; keys) methods as well.\n","date":"November 29, 2014","externalUrl":null,"permalink":"/posts/simple-guava-forwardingcache-i-use-when-developing-value-loaders/","section":"Posts","summary":"A note about using Guava ForwardingCache when developing value loaders.","title":"Simple Guava ForwardingCache I use when developing value loaders","type":"posts"},{"content":"Sometimes I want to search a chain of exceptions (caught-\u0026gt;cause1-\u0026gt;cause2…) for an exception of a particular type. This isn’t a difficult problem, but having a simple utility makes it easier:\n1public class SearchCausesFor\u0026lt;T extends Throwable\u0026gt; 2{ 3 private Class\u0026lt;T\u0026gt; target; 4 5 private SearchCausesFor ( Class\u0026lt;T\u0026gt; target ) 6 { 7 this.target = target; 8 } 9 10 public static \u0026lt;T extends Throwable\u0026gt; SearchCausesFor\u0026lt;T\u0026gt; SEARCH ( Class\u0026lt;T\u0026gt; target ) 11 { 12 if ( target == null ) throw new IllegalArgumentException ( \u0026#34;target is null\u0026#34; ); 13 return new SearchCausesFor\u0026lt;T\u0026gt; ( target ); 14 } 15 16 public T IN ( Throwable source ) 17 { 18 if ( source == null ) return null; 19 if ( target == null ) throw new IllegalStateException ( \u0026#34;target is null\u0026#34; ); 20 if ( target.isInstance( source ) ) return target.cast(source); 21 return IN ( source.getCause() ); 22 } 23} It’s pretty easy to use:\n1try 2{ 3 ... // some code that throws an exception 4} 5catch ( RootExceptionType oops ) 6{ 7 ExceptionTypeIWant wanted = SearchCausesFor.SEARCH(ExceptionTypeIWant.class).IN(oops); 8 if ( wanted != null ) // do something with wanted 9} ","date":"August 6, 2014","externalUrl":null,"permalink":"/posts/helpful-utility-for-searching-a-java-exception-chain-for-a-particular-exception-type/","section":"Posts","summary":"A small Java helper for finding a specific exception type in a cause chain.","title":"Helpful utility for searching a Java exception chain for a particular exception type","type":"posts"},{"content":"","date":"July 8, 2014","externalUrl":null,"permalink":"/tags/antlr4/","section":"Tags","summary":"","title":"Antlr4","type":"tags"},{"content":"By default a org.antlr.v4.runtime.Parser instance, as a subclass of org.antlr.v4.runtime.Recognizer, will have a org.antlr.v4.runtime.ConsoleErrorListener attached to it. Error messages will be piped through this listener and out to System.err, where they will be lost forever.\nYou can silence this output by calling removeErrorListeners() before parsing, and if you want to capture the error messages for parsing and display, you can add a custom BaseErrorListener implementation.\nHere’s a simple one that keeps every error in a List`:\n1public class DSLErrorListener extends BaseErrorListener 2{ 3 private List\u0026lt;SyntaxErrorItem\u0026gt; items; 4 5 public DSLErrorListener ( ) 6 { 7 this.items = new ArrayList\u0026lt;SyntaxErrorItem\u0026gt;(); 8 } 9 10 @Override 11 public void syntaxError ( Recognizer\u0026lt;?, ?\u0026gt; recognizer, 12 Object offendingSymbol, 13 int line, 14 int charPositionInLine, 15 String msg, 16 RecognitionException e ) 17 { 18 items.add ( new SyntaxErrorItem ( line, 19 charPositionInLine, 20 msg, 21 offendingSymbol, 22 e ) 23 ); 24 } 25 26 public boolean hasErrors ( ) 27 { 28 return this.items.size() \u0026gt; 0; 29 } 30 31 @Override 32 public String toString ( ) 33 { 34 if ( !hasErrors() ) return \u0026#34;0 errors\u0026#34;; 35 StringBuilder builder = new StringBuilder(); 36 for ( SyntaxErrorItem s : items ) 37 { 38 builder.append ( String.format ( \u0026#34;%s\\n\u0026#34;, s ) ); 39 } 40 return builder.toString(); 41 } 42} 43 44class SyntaxErrorItem 45{ 46 private int line; 47 48 private Object offendingSymbol; 49 50 private int column; 51 52 private String msg; 53 54 private RecognitionException oops; 55 56 SyntaxErrorItem ( int line, 57 int column, 58 String msg, 59 Object symbol, 60 RecognitionException oops ) 61 { 62 this.line = line; 63 this.column = column; 64 this.msg = msg; 65 this.offendingSymbol = symbol; 66 this.oops = oops; 67 } 68 69 @Override 70 public String toString() 71 { 72 if ( oops == null ) return String.format ( \u0026#34;[%d:%d] %s\u0026#34;, line, column, msg ); 73 else { 74 StringWriter sw = new StringWriter(); 75 PrintWriter pw = new PrintWriter ( sw ); 76 oops.printStackTrace(pw); 77 pw.close(); 78 return String.format ( \u0026#34;[%d:%d] %s\\n%s\u0026#34;, line, column, msg, sw.toString() ); 79 } 80 } 81} You would add it to a Parser using the addErrorListener method.\nHere’s a complete example:\n1ANTLRInputStream input = ...; 2Lexer lexer = ...; 3CommonTokenStream tokens = new CommonTokenStream ( lexer ); 4DSLErrorListener errorListener = new DSLErrorListener(); 5Parser parser = ...; 6parser.removeErrorListeners(); 7parser.addErrorListener ( errorListener ); 8ParseTree tree = ...; // Usually the parser subclass top-level rule name 9if ( errorListener.hasErrors() ) System.err.printf( \u0026#34;%s\\n\u0026#34;, errorListener ); // Or parse the errors: up to you 10else { 11 ParseTreeWalker walker = new ParseTreeWalker(); 12 // Walk the tree or visit it 13} ","date":"July 8, 2014","externalUrl":null,"permalink":"/posts/how-to-silence-antlr4s-error-messages/","section":"Posts","summary":"A short note about suppressing ANTLR4 parser error output.","title":"How to silence ANTLR4's error messages","type":"posts"},{"content":"It’s up on GitHub\n","date":"July 8, 2014","externalUrl":null,"permalink":"/posts/rewrote-the-antlr4-testrig-using-a-fluent-api/","section":"Posts","summary":"A note about rewriting the ANTLR4 TestRig around a fluent API.","title":"Rewrote the ANTLR4 TestRig using a fluent api","type":"posts"},{"content":"If you’re using an R800 on the mac for a custom application along with my CSTEventManager library, the following keycodes can be used. Don’t forget that you should configure the R800 as an ISO keyboard.\nCode Number Description 116 The Previous Slide button 121 The Next Slide button 96 Start/Stop Presentation toggle button 47 Blank screen toggle button ","date":"March 12, 2014","externalUrl":null,"permalink":"/posts/keycode-constants-for-the-logitech-r800-professional-presentation/","section":"Posts","summary":"A note collecting keycode constants for the Logitech R800 presenter.","title":"Keycode constants for the Logitech R800 Professional Presentation","type":"posts"},{"content":"","date":"March 12, 2014","externalUrl":null,"permalink":"/tags/logitech/","section":"Tags","summary":"","title":"Logitech","type":"tags"},{"content":"Sometimes I write web applications for Tomcat where I don’t want to report any errors beyond the HTTP status code. The quickest method for disabling those messages is configuring a Valve on the host section of the server.xml configuration file.\nIf you subclass the org.apache.catalina.valves.ErrorReportValve class (it’s in your Tomcat’s lib/catalina.jar) and override the report method like this:\n1public class ErrorSuppressorValve extends ErrorReportValve 2{ 3 protected void report(Request request, 4 Response response, 5 Throwable throwable) 6 { 7 // Override the default behavior by doing nothing 8 } 9} The client still receives the HTTP status code and anything already buffered, but it hides all of the automatically generated Tomcat error information.\nTo install it, just JAR up the class and put it in your Tomcat’s lib folder, then add the errorReportValveClass attribute to your Tomcat’s server.xml file.\n","date":"December 4, 2013","externalUrl":null,"permalink":"/posts/suppressing-tomcat-error-messages-for-all-applications-on-a-host/","section":"Posts","summary":"A note about suppressing Tomcat error output across applications on a host.","title":"Suppressing Tomcat error messages for all applications on a host","type":"posts"},{"content":"","date":"December 4, 2013","externalUrl":null,"permalink":"/tags/tomcat/","section":"Tags","summary":"","title":"Tomcat","type":"tags"}]