robots.txt Generator — Java source
Build a standards-compliant robots.txt with per-user-agent allow/disallow rules, crawl-delay, and sitemap entries.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// robots-txt-generator — Java port: standards-compliant robots.txt generator + parser.
//
// Display port of the CosmoDev Robots.txt Generator tool — same contract as
// cli/robots-txt-generator/robots-txt-generator.go (the live Go twin) and
// src/lib/robotsTxt.ts (canonical TypeScript). Pure + deterministic; JDK only.
// A group carries a LIST of user-agents (the REP spec's stacked User-agent
// lines); parsing aggregates repeated blocks per agent, then re-merges agents
// whose accumulated rules are identical into one stacked group, like the TS lib.
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
final class RobotsTxtGenerator {
// Package-private: the polyglot display filename is java.java, so the
// top-level class cannot be public (JLS §7.6 filename rule).
private RobotsTxtGenerator() {}
/** One REP group — mirrors {@code RuleGroup} in the Go twin. */
public static final class RuleGroup {
/** {@code ["*"]} or {@code ["Googlebot", "Bingbot"]} — stacked agents. */
public final List<String> userAgents;
/** Paths; an empty element renders as a bare {@code "Disallow:"}. */
public final List<String> disallow;
/** Paths to allow. */
public final List<String> allow;
/** Seconds between requests; {@code null} = unset, non-finite never emitted. */
public final Double crawlDelay;
public RuleGroup(List<String> userAgents, List<String> disallow, List<String> allow, Double crawlDelay) {
this.userAgents = userAgents;
this.disallow = disallow;
this.allow = allow;
this.crawlDelay = crawlDelay;
}
}
/** Full document: ordered rule groups + sitemap URLs. */
public static final class RobotsConfig {
public final List<RuleGroup> groups;
public final List<String> sitemaps;
public RobotsConfig(List<RuleGroup> groups, List<String> sitemaps) {
this.groups = groups;
this.sitemaps = sitemaps;
}
}
private static final Pattern COMMENT = Pattern.compile("#.*$");
private static final Pattern BLANK_RUN = Pattern.compile("\n{3,}");
// cleanAgents in the TS lib: trim + drop blanks. A non-empty list that
// trims away entirely degrades to ["*"]; an empty list means "no group yet".
private static List<String> cleanAgents(List<String> list) {
List<String> out = new ArrayList<>();
for (String u : list) {
String t = u.trim();
if (!t.isEmpty()) out.add(t);
}
if (out.isEmpty() && !list.isEmpty()) out.add("*");
return out;
}
// JS-style number rendering for Crawl-delay: 10.0 -> "10", 1.5 -> "1.5".
private static String numStr(double f) {
return f == Math.floor(f) ? String.valueOf((long) f) : String.valueOf(f);
}
public static String generateRobots(RobotsConfig config) {
List<String> out = new ArrayList<>();
for (RuleGroup g : config.groups) {
List<String> uas = cleanAgents(g.userAgents);
if (uas.isEmpty()) continue;
for (String ua : uas) out.add("User-agent: " + ua);
for (String a : g.allow) {
String p = a.trim();
if (!p.isEmpty()) out.add("Allow: " + p);
}
if (g.disallow.isEmpty()) {
out.add("Disallow:"); // no entries -> the allow-all marker
} else {
for (String d : g.disallow) out.add("Disallow: " + d); // "" kept verbatim
}
if (g.crawlDelay != null && Double.isFinite(g.crawlDelay)) {
out.add("Crawl-delay: " + numStr(g.crawlDelay));
}
out.add(""); // blank line separates groups
}
for (String s : config.sitemaps) {
String url = s.trim();
if (!url.isEmpty()) out.add("Sitemap: " + url);
}
return BLANK_RUN.matcher(String.join("\n", out)).replaceAll("\n\n").stripTrailing() + "\n";
}
private static final class Acc {
final String ua;
final List<String> disallow = new ArrayList<>(), allow = new ArrayList<>();
Double crawlDelay;
Acc(String ua) { this.ua = ua; }
}
// Moves the pending User-agent stack into per-agent rule buckets (Google's
// merge semantics: a rule line applies to every agent of the immediately
// preceding consecutive User-agent stack).
private static List<Acc> flush(List<String> stack, LinkedHashMap<String, Acc> perUA) {
List<Acc> current = new ArrayList<>();
for (String ua : stack) current.add(perUA.computeIfAbsent(ua, k -> new Acc(k)));
stack.clear();
return current;
}
// ParseRobots in the Go twin: strip '#'-comments, split on the FIRST ':'
// (values may contain colons), unknown directives ignored. Garbage
// Crawl-delay values coerce to NaN like JS Number() — never re-emitted.
public static RobotsConfig parseRobots(String text) {
LinkedHashMap<String, Acc> perUA = new LinkedHashMap<>(); // insertion-ordered
List<String> stack = new ArrayList<>(), sitemaps = new ArrayList<>();
List<Acc> current = new ArrayList<>();
for (String raw : (text == null ? "" : text).split("\n", -1)) {
String line = COMMENT.matcher(raw).replaceAll("").trim();
if (line.isEmpty()) continue;
int colon = line.indexOf(':');
if (colon < 0) continue;
String field = line.substring(0, colon).trim().toLowerCase();
String value = line.substring(colon + 1).trim();
switch (field) {
case "user-agent" -> {
String ua = value.isEmpty() ? "*" : value;
if (!stack.contains(ua)) stack.add(ua);
}
case "disallow" -> { if (!stack.isEmpty()) current = flush(stack, perUA); current.forEach(a -> a.disallow.add(value)); }
case "allow" -> { if (!stack.isEmpty()) current = flush(stack, perUA); current.forEach(a -> a.allow.add(value)); }
case "crawl-delay" -> {
if (!stack.isEmpty()) current = flush(stack, perUA);
double v;
try { v = Double.parseDouble(value); } catch (NumberFormatException e) { v = Double.NaN; }
final double fv = v;
current.forEach(a -> a.crawlDelay = fv);
}
case "sitemap" -> { if (!stack.isEmpty()) current = flush(stack, perUA); sitemaps.add(value); }
default -> { }
}
}
// Merge agents with identical rule sets into one stacked group.
List<RuleGroup> groups = new ArrayList<>();
for (Map.Entry<String, Acc> e : perUA.entrySet()) {
Acc acc = e.getValue();
RuleGroup twin = null;
for (RuleGroup g : groups) {
if (g.disallow.equals(acc.disallow) && g.allow.equals(acc.allow)
&& Objects.equals(g.crawlDelay, acc.crawlDelay)) { twin = g; break; }
}
if (twin != null) twin.userAgents.add(e.getKey()); // lists are mutable for the merge
else groups.add(new RuleGroup(new ArrayList<>(List.of(e.getKey())), new ArrayList<>(acc.disallow),
new ArrayList<>(acc.allow), acc.crawlDelay));
}
return new RobotsConfig(groups, sitemaps);
}
public static void main(String[] args) {
// Sample config -> generated body -> parse round-trip check (tool page behavior).
RobotsConfig config = new RobotsConfig(List.of(
new RuleGroup(new ArrayList<>(List.of("*")), new ArrayList<>(List.of("/private/", "/admin/")),
new ArrayList<>(List.of("/admin/public/")), null),
new RuleGroup(new ArrayList<>(List.of("GPTBot", "CCBot")), new ArrayList<>(List.of("/")),
new ArrayList<>(), 10.0)),
List.of("https://example.com/sitemap.xml"));
String body = generateRobots(config);
System.out.print(body);
System.out.println("round-trip stable: " + generateRobots(parseRobots(body)).equals(body));
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →