diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/AbstractGenerator.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/AbstractGenerator.java index 1d92c923c5c..40b59435c44 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/AbstractGenerator.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/AbstractGenerator.java @@ -99,6 +99,16 @@ public abstract class AbstractGenerator { } } + public String readResourceContents(String resourceFilePath) { + StringBuilder sb = new StringBuilder(); + Scanner scanner = new Scanner(this.getClass().getResourceAsStream(getCPResourcePath(resourceFilePath)), "UTF-8"); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + sb.append(line).append('\n'); + } + return sb.toString(); + } + public boolean embeddedTemplateExists(String name) { return this.getClass().getClassLoader().getResource(getCPResourcePath(name)) != null; } diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConfig.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConfig.java index 3dc90472247..72a2e50d149 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConfig.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenConfig.java @@ -184,4 +184,5 @@ public interface CodegenConfig { String getHttpUserAgent(); + String getCommonTemplateDir(); } diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java index 15eed39154a..8317145d0d9 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java @@ -94,6 +94,7 @@ public class DefaultCodegen { protected Map modelDocTemplateFiles = new HashMap(); protected String templateDir; protected String embeddedTemplateDir; + protected String commonTemplateDir = "_common"; protected Map additionalProperties = new HashMap(); protected Map vendorExtensions = new HashMap(); protected List supportingFiles = new ArrayList(); @@ -441,6 +442,14 @@ public class DefaultCodegen { } } + public String getCommonTemplateDir() { + return this.commonTemplateDir; + } + + public void setCommonTemplateDir(String commonTemplateDir) { + this.commonTemplateDir = commonTemplateDir; + } + public Map apiDocTemplateFiles() { return apiDocTemplateFiles; } diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java index e6bef99468d..b648ce3f870 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultGenerator.java @@ -2,6 +2,7 @@ package io.swagger.codegen; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template; +import io.swagger.codegen.ignore.CodegenIgnoreProcessor; import io.swagger.models.*; import io.swagger.models.auth.OAuth2Definition; import io.swagger.models.auth.SecuritySchemeDefinition; @@ -24,6 +25,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { protected CodegenConfig config; protected ClientOptInput opts; protected Swagger swagger; + protected CodegenIgnoreProcessor ignoreProcessor; @Override public Generator opts(ClientOptInput opts) { @@ -31,8 +33,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { this.swagger = opts.getSwagger(); this.config = opts.getConfig(); + this.config.additionalProperties().putAll(opts.getOpts().getProperties()); + ignoreProcessor = new CodegenIgnoreProcessor(this.config.getOutputDir()); + return this; } @@ -197,7 +202,6 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { String basePath = hostBuilder.toString(); String basePathWithoutHost = swagger.getBasePath(); - // resolve inline models InlineModelResolver inlineModelResolver = new InlineModelResolver(); inlineModelResolver.flatten(swagger); @@ -305,19 +309,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { LOGGER.info("Skipped overwriting " + filename); continue; } - String templateFile = getFullTemplateFile(config, templateName); - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); - writeToFile(filename, tmpl.execute(models)); - files.add(new File(filename)); + + File written = processTemplateToFile(models, templateName, filename); + if(written != null) { + files.add(written); + } } if(generateModelTests) { @@ -330,19 +326,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { LOGGER.info("File exists. Skipped overwriting " + filename); continue; } - String templateFile = getFullTemplateFile(config, templateName); - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); - writeToFile(filename, tmpl.execute(models)); - files.add(new File(filename)); + + File written = processTemplateToFile(models, templateName, filename); + if (written != null) { + files.add(written); + } } } @@ -355,19 +343,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { LOGGER.info("Skipped overwriting " + filename); continue; } - String templateFile = getFullTemplateFile(config, templateName); - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); - writeToFile(filename, tmpl.execute(models)); - files.add(new File(filename)); + + File written = processTemplateToFile(models, templateName, filename); + if (written != null) { + files.add(written); + } } } } catch (Exception e) { @@ -443,20 +423,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { continue; } - String templateFile = getFullTemplateFile(config, templateName); - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); - - writeToFile(filename, tmpl.execute(operation)); - files.add(new File(filename)); + File written = processTemplateToFile(operation, templateName, filename); + if(written != null) { + files.add(written); + } } if(generateApiTests) { @@ -468,23 +438,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { LOGGER.info("File exists. Skipped overwriting " + filename); continue; } - String templateFile = getFullTemplateFile(config, templateName); - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); - writeToFile(filename, tmpl.execute(operation)); - files.add(new File(filename)); + File written = processTemplateToFile(operation, templateName, filename); + if (written != null) { + files.add(written); + } } } + if(generateApiDocumentation) { // to generate api documentation files for (String templateName : config.apiDocTemplateFiles().keySet()) { @@ -494,20 +456,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { continue; } - String templateFile = getFullTemplateFile(config, templateName); - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); - - writeToFile(filename, tmpl.execute(operation)); - files.add(new File(filename)); + File written = processTemplateToFile(operation, templateName, filename); + if (written != null) { + files.add(written); + } } } @@ -590,53 +542,95 @@ public class DefaultGenerator extends AbstractGenerator implements Generator { } } if(shouldGenerate) { - if (templateFile.endsWith("mustache")) { - String template = readTemplate(templateFile); - Template tmpl = Mustache.compiler() - .withLoader(new Mustache.TemplateLoader() { - @Override - public Reader getTemplate(String name) { - return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); - } - }) - .defaultValue("") - .compile(template); + if(ignoreProcessor.allowsFile(new File(outputFilename))) { + if (templateFile.endsWith("mustache")) { + String template = readTemplate(templateFile); + Template tmpl = Mustache.compiler() + .withLoader(new Mustache.TemplateLoader() { + @Override + public Reader getTemplate(String name) { + return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); + } + }) + .defaultValue("") + .compile(template); - writeToFile(outputFilename, tmpl.execute(bundle)); - files.add(new File(outputFilename)); - } else { - InputStream in = null; - - try { - in = new FileInputStream(templateFile); - } catch (Exception e) { - // continue - } - if (in == null) { - in = this.getClass().getClassLoader().getResourceAsStream(getCPResourcePath(templateFile)); - } - File outputFile = new File(outputFilename); - OutputStream out = new FileOutputStream(outputFile, false); - if (in != null) { - LOGGER.info("writing file " + outputFile); - IOUtils.copy(in, out); + writeToFile(outputFilename, tmpl.execute(bundle)); + files.add(new File(outputFilename)); } else { - if (in == null) { - LOGGER.error("can't open " + templateFile + " for input"); + InputStream in = null; + + try { + in = new FileInputStream(templateFile); + } catch (Exception e) { + // continue } + if (in == null) { + in = this.getClass().getClassLoader().getResourceAsStream(getCPResourcePath(templateFile)); + } + File outputFile = new File(outputFilename); + OutputStream out = new FileOutputStream(outputFile, false); + if (in != null) { + LOGGER.info("writing file " + outputFile); + IOUtils.copy(in, out); + } else { + if (in == null) { + LOGGER.error("can't open " + templateFile + " for input"); + } + } + files.add(outputFile); } - files.add(outputFile); + } else { + LOGGER.info("Skipped generation of " + outputFilename + " due to rule in .swagger-codegen-ignore"); } } } catch (Exception e) { throw new RuntimeException("Could not generate supporting file '" + support + "'", e); } } + + // Consider .swagger-codegen-ignore a supporting file + // Output .swagger-codegen-ignore if it doesn't exist and wasn't explicitly created by a generator + final String swaggerCodegenIgnore = ".swagger-codegen-ignore"; + String ignoreFileNameTarget = config.outputFolder() + File.separator + swaggerCodegenIgnore; + File ignoreFile = new File(ignoreFileNameTarget); + if(!ignoreFile.exists()) { + String ignoreFileNameSource = File.separator + config.getCommonTemplateDir() + File.separator + swaggerCodegenIgnore; + String ignoreFileContents = readResourceContents(ignoreFileNameSource); + try { + writeToFile(ignoreFileNameTarget, ignoreFileContents); + } catch (IOException e) { + throw new RuntimeException("Could not generate supporting file '" + swaggerCodegenIgnore + "'", e); + } + files.add(ignoreFile); + } } config.processSwagger(swagger); return files; } + private File processTemplateToFile(Map templateData, String templateName, String outputFilename) throws IOException { + if(ignoreProcessor.allowsFile(new File(outputFilename.replaceAll("//", "/")))) { + String templateFile = getFullTemplateFile(config, templateName); + String template = readTemplate(templateFile); + Template tmpl = Mustache.compiler() + .withLoader(new Mustache.TemplateLoader() { + @Override + public Reader getTemplate(String name) { + return getTemplateReader(getFullTemplateFile(config, name + ".mustache")); + } + }) + .defaultValue("") + .compile(template); + + writeToFile(outputFilename, tmpl.execute(templateData)); + return new File(outputFilename); + } + + LOGGER.info("Skipped generation of " + outputFilename + " due to rule in .swagger-codegen-ignore"); + return null; + } + private static void processMimeTypes(List mimeTypeList, Map operation, String source) { if (mimeTypeList != null && mimeTypeList.size() > 0) { List> c = new ArrayList>(); diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/CodegenIgnoreProcessor.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/CodegenIgnoreProcessor.java new file mode 100644 index 00000000000..fb25ac82b28 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/CodegenIgnoreProcessor.java @@ -0,0 +1,134 @@ +package io.swagger.codegen.ignore; + +import com.google.common.collect.ImmutableList; +import io.swagger.codegen.ignore.rules.DirectoryRule; +import io.swagger.codegen.ignore.rules.Rule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +public class CodegenIgnoreProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(CodegenIgnoreProcessor.class); + private final String outputPath; + private List exclusionRules = new ArrayList<>(); + private List inclusionRules = new ArrayList<>(); + + public CodegenIgnoreProcessor(String outputPath) { + this.outputPath = outputPath; + final File directory = new File(outputPath); + if(directory.exists() && directory.isDirectory()){ + final File codegenIgnore = new File(directory, ".swagger-codegen-ignore"); + if(codegenIgnore.exists() && codegenIgnore.isFile()){ + try { + loadCodegenRules(codegenIgnore); + } catch (IOException e) { + LOGGER.error("Could not process .swagger-codegen-ignore.", e.getMessage()); + } + } else { + // log info message + LOGGER.info("No .swagger-codegen-ignore file found."); + } + } + } + + void loadCodegenRules(File codegenIgnore) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(codegenIgnore))) { + String line; + + // NOTE: Comments that start with a : (e.g. //:) are pulled from git documentation for .gitignore + // see: https://github.com/git/git/blob/90f7b16b3adc78d4bbabbd426fb69aa78c714f71/Documentation/gitignore.txt + while ((line = reader.readLine()) != null) { + if( + //: A blank line matches no files, so it can serve as a separator for readability. + line.length() == 0 + ) continue; + + Rule rule = Rule.create(line); + + // rule could be null here if it's a COMMENT, for example + if(rule != null) { + if (Boolean.TRUE.equals(rule.getNegated())) { + inclusionRules.add(rule); + } else { + exclusionRules.add(rule); + } + } + } + } + } + + public boolean allowsFile(File targetFile) { + File file = new File(new File(this.outputPath).toURI().relativize(targetFile.toURI()).getPath()); + Boolean directoryExcluded = false; + Boolean exclude = false; + if(exclusionRules.size() == 0 && inclusionRules.size() == 0) { + return true; + } + + // NOTE: We *must* process all exclusion rules + for (int i = 0; i < exclusionRules.size(); i++) { + Rule current = exclusionRules.get(i); + Rule.Operation op = current.evaluate(file.getPath()); + + switch (op){ + case EXCLUDE: + exclude = true; + + // Include rule can't override rules that exclude a file by some parent directory. + if(current instanceof DirectoryRule) { + directoryExcluded = true; + } + break; + case INCLUDE: + // This won't happen here. + break; + case NOOP: + break; + case EXCLUDE_AND_TERMINATE: + i = exclusionRules.size(); + break; + } + } + + if(exclude) { + // Only need to process inclusion rules if we've been excluded + for (int i = 0; exclude && i < inclusionRules.size(); i++) { + Rule current = inclusionRules.get(i); + Rule.Operation op = current.evaluate(file.getPath()); + + // At this point exclude=true means the file should be ignored. + // op == INCLUDE means we have to flip that flag. + if(op.equals(Rule.Operation.INCLUDE)) { + if(current instanceof DirectoryRule && directoryExcluded) { + // e.g + // baz/ + // !foo/bar/baz/ + // NOTE: Possibly surprising side effect: + // foo/bar/baz/ + // !bar/ + exclude = false; + } else if (!directoryExcluded) { + // e.g. + // **/*.log + // !ISSUE_1234.log + exclude = false; + } + } + } + } + + return Boolean.FALSE.equals(exclude); + } + + public List getInclusionRules() { + return ImmutableList.copyOf(inclusionRules); + } + + public List getExclusionRules() { + return ImmutableList.copyOf(exclusionRules); + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/DirectoryRule.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/DirectoryRule.java new file mode 100644 index 00000000000..2619f1f1c38 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/DirectoryRule.java @@ -0,0 +1,20 @@ +package io.swagger.codegen.ignore.rules; + +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.util.List; + +public class DirectoryRule extends FileRule { + + private PathMatcher matcher = null; + + DirectoryRule(List syntax, String definition) { + super(syntax, definition); + matcher = FileSystems.getDefault().getPathMatcher("glob:**/"+this.getPattern()); + } + + @Override + public Boolean matches(String relativePath) { + return matcher.matches(FileSystems.getDefault().getPath(relativePath)); + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/EverythingRule.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/EverythingRule.java new file mode 100644 index 00000000000..f942cb222ce --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/EverythingRule.java @@ -0,0 +1,20 @@ +package io.swagger.codegen.ignore.rules; + +import java.util.List; + +/** + * An ignore rule which matches everything. + */ +public class EverythingRule extends Rule { + EverythingRule(List syntax, String definition) { + super(syntax, definition); + } + + @Override + public Boolean matches(String relativePath) { + return true; + } + + @Override + protected Operation getExcludeOperation(){ return Operation.EXCLUDE_AND_TERMINATE; } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/FileRule.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/FileRule.java new file mode 100644 index 00000000000..a97513dc1e7 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/FileRule.java @@ -0,0 +1,20 @@ +package io.swagger.codegen.ignore.rules; + +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.util.List; + +public class FileRule extends Rule { + + private PathMatcher matcher = null; + + FileRule(List syntax, String definition) { + super(syntax, definition); + matcher = FileSystems.getDefault().getPathMatcher("glob:"+this.getPattern()); + } + + @Override + public Boolean matches(String relativePath) { + return matcher.matches(FileSystems.getDefault().getPath(relativePath)); + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/IgnoreLineParser.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/IgnoreLineParser.java new file mode 100644 index 00000000000..d79bc03ae15 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/IgnoreLineParser.java @@ -0,0 +1,134 @@ +package io.swagger.codegen.ignore.rules; + +import java.util.ArrayList; +import java.util.List; + +public class IgnoreLineParser { + enum Token { + MATCH_ALL("**"), + MATCH_ANY("*"), + ESCAPED_EXCLAMATION("\\!"), + ESCAPED_SPACE("\\ "), + PATH_DELIM("/"), + NEGATE("!"), + TEXT(null), + DIRECTORY_MARKER("/"), + ROOTED_MARKER("/"), + COMMENT(null); + + private String pattern; + + Token(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } + } + + // NOTE: Comments that start with a : (e.g. //:) are pulled from git documentation for .gitignore + // see: https://github.com/git/git/blob/90f7b16b3adc78d4bbabbd426fb69aa78c714f71/Documentation/gitignore.txt + static List parse(String text) throws ParserException { + List parts = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + String current = null; + String next = null; + + char[] characters = text.toCharArray(); + for (int i = 0, totalLength = characters.length; i < totalLength; i++) { + char character = characters[i]; + current = String.valueOf(character); + next = i < totalLength - 1 ? String.valueOf(characters[i + 1]) : null; + + if (i == 0) { + if ("#".equals(current)) { + //: A line starting with # serves as a comment. + parts.add(new Part(Token.COMMENT, text)); + i = totalLength; // rather than early return + continue; + } else if ("!".equals(current)) { + if (i == totalLength - 1) { + throw new ParserException("Negation with no negated pattern."); + } else { + parts.add(new Part(Token.NEGATE)); + continue; + } + } else if ("\\".equals(current) && "#".equals(next)) { + //: Put a backslash ("`\`") in front of the first hash for patterns + //: that begin with a hash. + // NOTE: Just push forward and drop the escape character. Falls through to TEXT token. + current = next; + next = null; + i++; + } + } + + if (Token.MATCH_ANY.pattern.equals(current)) { + + if (Token.MATCH_ANY.pattern.equals(next)) { + // peek ahead for invalid pattern. Slightly inefficient, but acceptable. + if ((i+2 < totalLength - 1) && + String.valueOf(characters[i+2]).equals(Token.MATCH_ANY.pattern)) { + // It doesn't matter where we are in the pattern, *** is invalid. + throw new ParserException("The pattern *** is invalid."); + } + + parts.add(new Part(Token.MATCH_ALL)); + i++; + continue; + } else { + parts.add(new Part(Token.MATCH_ANY)); + continue; + } + } + + if (i == 0 && Token.ROOTED_MARKER.pattern.equals(current)) { + parts.add(new Part(Token.ROOTED_MARKER)); + continue; + } + + if ("\\".equals(current) && " ".equals(next)) { + parts.add(new Part(Token.ESCAPED_SPACE)); + i++; + continue; + } else if ("\\".equals(current) && "!".equals(next)) { + parts.add(new Part(Token.ESCAPED_EXCLAMATION)); + i++; + continue; + } + + if (Token.PATH_DELIM.pattern.equals(current)) { + if (i != totalLength - 1) { + if (sb.length() > 0) { + parts.add(new Part(Token.TEXT, sb.toString())); + sb.delete(0, sb.length()); + } + + parts.add(new Part(Token.PATH_DELIM)); + if(Token.PATH_DELIM.pattern.equals(next)) { + // ignore doubled path delims. NOTE: doesn't do full lookahead, so /// will result in // + i++; + } + continue; + } else if (i == totalLength - 1) { + parts.add(new Part(Token.TEXT, sb.toString())); + sb.delete(0, sb.length()); + + parts.add(new Part(Token.DIRECTORY_MARKER)); + continue; + } + } + + sb.append(current); + } + + if (sb.length() > 0) { + // NOTE: All spaces escaped spaces are a special token, ESCAPED_SPACE + //: Trailing spaces are ignored unless they are quoted with backslash ("`\`") + parts.add(new Part(Token.TEXT, sb.toString().trim())); + } + + return parts; + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/InvalidRule.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/InvalidRule.java new file mode 100644 index 00000000000..46524f4795b --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/InvalidRule.java @@ -0,0 +1,26 @@ +package io.swagger.codegen.ignore.rules; + +import java.util.List; + +public class InvalidRule extends Rule { + private final String reason; + + InvalidRule(List syntax, String definition, String reason) { + super(syntax, definition); + this.reason = reason; + } + + @Override + public Boolean matches(String relativePath) { + return null; + } + + @Override + public Operation evaluate(String relativePath) { + return Operation.NOOP; + } + + public String getReason() { + return reason; + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/ParserException.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/ParserException.java new file mode 100644 index 00000000000..03e02d8fd70 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/ParserException.java @@ -0,0 +1,15 @@ +package io.swagger.codegen.ignore.rules; + +public class ParserException extends Exception { + /** + * Constructs a new exception with the specified detail message. The + * cause is not initialized, and may subsequently be initialized by + * a call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public ParserException(String message) { + super(message); + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/Part.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/Part.java new file mode 100644 index 00000000000..71379922175 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/Part.java @@ -0,0 +1,24 @@ +package io.swagger.codegen.ignore.rules; + +class Part { + private final IgnoreLineParser.Token token; + private final String value; + + public Part(IgnoreLineParser.Token token, String value) { + this.token = token; + this.value = value; + } + + public Part(IgnoreLineParser.Token token) { + this.token = token; + this.value = token.getPattern(); + } + + public IgnoreLineParser.Token getToken() { + return token; + } + + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/RootedFileRule.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/RootedFileRule.java new file mode 100644 index 00000000000..6c445b6299a --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/RootedFileRule.java @@ -0,0 +1,58 @@ +package io.swagger.codegen.ignore.rules; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * A special case rule which matches files only if they're located + * in the same directory as the .swagger-codegen-ignore file. + */ +public class RootedFileRule extends Rule { + private String definedFilename = null; + private String definedExtension = null; + + RootedFileRule(List syntax, String definition) { + super(syntax, definition); + + int separatorIndex = definition.lastIndexOf("."); + definedFilename = getFilenamePart(definition, separatorIndex); + definedExtension = getExtensionPart(definition, separatorIndex); + } + + private String getFilenamePart(final String input, int stopIndex){ + return input.substring('/' == input.charAt(0) ? 1 : 0, stopIndex > 0 ? stopIndex : input.length()); + } + + private String getExtensionPart(final String input, int stopIndex) { + return input.substring(stopIndex > 0 ? stopIndex+1: input.length(), input.length()); + } + + @Override + public Boolean matches(String relativePath) { + // NOTE: Windows-style separator isn't supported, so File.pathSeparator would be incorrect here. + // NOTE: lastIndexOf rather than contains because /file.txt is acceptable while path/file.txt is not. + // relativePath will be passed by CodegenIgnoreProcessor and is relative to .codegen-ignore. + boolean isSingleFile = relativePath.lastIndexOf("/") <= 0; + + if(isSingleFile) { + int separatorIndex = relativePath.lastIndexOf("."); + final String filename = getFilenamePart(relativePath, separatorIndex); + final String extension = getExtensionPart(relativePath, separatorIndex); + boolean extensionMatches = definedExtension.equals(extension) || definedExtension.equals(IgnoreLineParser.Token.MATCH_ANY.getPattern()); + + if(extensionMatches && definedFilename.contains(IgnoreLineParser.Token.MATCH_ANY.getPattern())) { + // TODO: Evaluate any other escape requirements here. + Pattern regex = Pattern.compile( + definedFilename + .replaceAll(Pattern.quote("."), "\\\\Q.\\\\E") + .replaceAll(Pattern.quote("*"), ".*?") // non-greedy match on 0+ any character + ); + return regex.matcher(filename).matches(); + } + + return extensionMatches && definedFilename.equals(filename); + } + + return false; + } +} diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/Rule.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/Rule.java new file mode 100644 index 00000000000..8d199cba763 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/ignore/rules/Rule.java @@ -0,0 +1,175 @@ +package io.swagger.codegen.ignore.rules; + +import java.util.List; + +public abstract class Rule { + + public enum Operation {EXCLUDE, INCLUDE, NOOP, EXCLUDE_AND_TERMINATE} + + // The original rule + private final String definition; + + private final List syntax; + + Rule(List syntax, String definition) { + this.syntax = syntax; + this.definition = definition; + } + + public abstract Boolean matches(String relativePath); + + public String getDefinition() { + return this.definition; + } + + protected String getPattern() { + if(syntax == null) return this.definition; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < syntax.size(); i++) { + Part current = syntax.get(i); + + switch(current.getToken()){ + case MATCH_ALL: + case MATCH_ANY: + case ESCAPED_EXCLAMATION: + case ESCAPED_SPACE: + case PATH_DELIM: + case TEXT: + case DIRECTORY_MARKER: + sb.append(current.getValue()); + break; + case NEGATE: + case ROOTED_MARKER: + case COMMENT: + break; + } + } + return sb.toString(); + } + + /** + * Whether or not the rule should be negated. !foo means foo should be removed from previous matches. + * Example: **\/*.bak excludes all backup. Adding !/test.bak will include test.bak in the project root. + *

+ * NOTE: It is not possible to re-include a file if a parent directory of that file is excluded. + */ + public Boolean getNegated() { + return this.syntax != null && this.syntax.size() > 0 && this.syntax.get(0).getToken() == IgnoreLineParser.Token.NEGATE; + } + + public Operation evaluate(String relativePath) { + if (Boolean.TRUE.equals(matches(relativePath))) { + if(Boolean.TRUE.equals(this.getNegated())) { + return this.getIncludeOperation(); + } + return this.getExcludeOperation(); + } + return Operation.NOOP; + } + + protected Operation getIncludeOperation(){ return Operation.INCLUDE; } + protected Operation getExcludeOperation(){ return Operation.EXCLUDE; } + + public static Rule create(String definition) { + // NOTE: Comments that start with a : (e.g. //:) are pulled from git documentation for .gitignore + // see: https://github.com/git/git/blob/90f7b16b3adc78d4bbabbd426fb69aa78c714f71/Documentation/gitignore.txt + Rule rule = null; + if (definition.equals(".")) { + return new InvalidRule(null, definition, "Pattern '.' is invalid."); + } else if (definition.equals("!.")) { + return new InvalidRule(null, definition, "Pattern '!.' is invalid."); + } else if (definition.startsWith("..")) { + return new InvalidRule(null, definition, "Pattern '..' is invalid."); + } + + try { + List result = IgnoreLineParser.parse(definition); + + Boolean directoryOnly = null; + if (result.size() == 0) { + return rule; + } else if (result.size() == 1) { + // single-character filename only + Part part = result.get(0); + if (IgnoreLineParser.Token.MATCH_ANY.equals(part.getToken())) { + rule = new RootedFileRule(result, definition); + } else { + rule = new FileRule(result, definition); + } + } else { + IgnoreLineParser.Token head = result.get(0).getToken(); + + //: An optional prefix "`!`" which negates the pattern; any + //: matching file excluded by a previous pattern will become + //: included again. It is not possible to re-include a file if a parent + //: directory of that file is excluded. Git doesn't list excluded + //: directories for performance reasons, so any patterns on contained + //: files have no effect, no matter where they are defined. + //: Put a backslash ("`\`") in front of the first "`!`" for patterns + //: that begin with a literal "`!`", for example, "`\!important!.txt`". + // see this.getNegated(); + + //: If the pattern ends with a slash, it is removed for the + //: purpose of the following description, but it would only find + //: a match with a directory. In other words, `foo/` will match a + //: directory `foo` and paths underneath it, but will not match a + //: regular file or a symbolic link `foo` (this is consistent + //: with the way how pathspec works in general in Git). + directoryOnly = IgnoreLineParser.Token.DIRECTORY_MARKER.equals(result.get(result.size() - 1).getToken()); + + if (directoryOnly) { + rule = new DirectoryRule(result, definition); + } else if (IgnoreLineParser.Token.PATH_DELIM.equals(head)) { + //: A leading slash matches the beginning of the pathname. + //: For example, "/{asterisk}.c" matches "cat-file.c" but not + //: "mozilla-sha1/sha1.c". + rule = new RootedFileRule(result, definition); + } else { + // case 1 + //: If the pattern does not contain a slash '/', Git treats it as + //: a shell glob pattern and checks for a match against the + //: pathname relative to the location of the `.gitignore` file + //: (relative to the toplevel of the work tree if not from a + //: `.gitignore` file). + + // case 2 + //: Otherwise, Git treats the pattern as a shell glob suitable + //: for consumption by fnmatch(3) with the FNM_PATHNAME flag: + //: wildcards in the pattern will not match a / in the pathname. + //: For example, "Documentation/{asterisk}.html" matches + //: "Documentation/git.html" but not "Documentation/ppc/ppc.html" + //: or "tools/perf/Documentation/perf.html". + + + // case 3 + //: Two consecutive asterisks ("`**`") in patterns matched against + //: full pathname may have special meaning: + //: + //: - A leading "`**`" followed by a slash means match in all + //: directories. For example, "`**/foo`" matches file or directory + //: "`foo`" anywhere, the same as pattern "`foo`". "`**/foo/bar`" + //: matches file or directory "`bar`" anywhere that is directly + //: under directory "`foo`". + //: + //: - A trailing "`/**`" matches everything inside. For example, + //: "`abc/**`" matches all files inside directory "`abc`", relative + //: to the location of the `.gitignore` file, with infinite depth. + //: + //: - A slash followed by two consecutive asterisks then a slash + //: matches zero or more directories. For example, "`a/**/b`" + //: matches "`a/b`", "`a/x/b`", "`a/x/y/b`" and so on. + //: + //: - Other consecutive asterisks are considered invalid. + rule = new FileRule(result, definition); + } + + } + } catch (ParserException e) { + e.printStackTrace(); + return new InvalidRule(null, definition, e.getMessage()); + } + + return rule; + } +} diff --git a/modules/swagger-codegen/src/main/resources/_common/.swagger-codegen-ignore b/modules/swagger-codegen/src/main/resources/_common/.swagger-codegen-ignore new file mode 100644 index 00000000000..19d3377182e --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/_common/.swagger-codegen-ignore @@ -0,0 +1,23 @@ +# Swagger Codegen Ignore +# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# Thsi matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/CodegenIgnoreProcessorTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/CodegenIgnoreProcessorTest.java new file mode 100644 index 00000000000..a9fe89214d5 --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/CodegenIgnoreProcessorTest.java @@ -0,0 +1,21 @@ +package io.swagger.codegen.ignore; + +import org.testng.annotations.Test; + +public class CodegenIgnoreProcessorTest { + @Test + public void loadCodegenRules() throws Exception { + + } + + @Test + public void getInclusionRules() throws Exception { + + } + + @Test + public void getExclusionRules() throws Exception { + + } + +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/FileRuleTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/FileRuleTest.java new file mode 100644 index 00000000000..f093507d84c --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/FileRuleTest.java @@ -0,0 +1,70 @@ +package io.swagger.codegen.ignore.rules; + +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.*; + +public class FileRuleTest { + @Test + public void testMatchComplex() throws Exception { + // Arrange + final String definition = "path/to/**/complex/*.txt"; + final String relativePath = "path/to/some/nested/complex/xyzzy.txt"; + + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "path"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.TEXT, "to"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.MATCH_ALL), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.TEXT, "complex"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".txt") + ); + + Rule rule = new FileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testNonMatchComplex() throws Exception { + // Arrange + final String definition = "path/to/**/complex/*.txt"; + final String relativePath = "path/to/some/nested/invalid/xyzzy.txt"; + + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "path"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.TEXT, "to"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.MATCH_ALL), + new Part(IgnoreLineParser.Token.TEXT, "complex"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".txt") + ); + + Rule rule = new FileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } + +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/IgnoreLineParserTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/IgnoreLineParserTest.java new file mode 100644 index 00000000000..17a96932d72 --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/IgnoreLineParserTest.java @@ -0,0 +1,158 @@ +package io.swagger.codegen.ignore.rules; + +import org.testng.annotations.Test; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import static org.testng.Assert.*; + +public class IgnoreLineParserTest { + private IgnoreLineParser.Token verifyInputToSingleToken(final String input, IgnoreLineParser.Token token) throws ParserException { + // Act + List result = IgnoreLineParser.parse(input); + + // Assert + assertNotNull(result); + assertEquals(result.size(), 1); + IgnoreLineParser.Token actual = result.get(0).getToken(); + assertEquals(actual, token); + + return actual; + } + + @Test + public void parseMatchAll() throws Exception { + verifyInputToSingleToken("**", IgnoreLineParser.Token.MATCH_ALL); + } + + @Test + public void parseMatchAny() throws Exception { + verifyInputToSingleToken("*", IgnoreLineParser.Token.MATCH_ANY); + } + + @Test(expectedExceptions = ParserException.class, + expectedExceptionsMessageRegExp = "Negation with no negated pattern\\.") + public void parseNegate() throws Exception { + verifyInputToSingleToken("!", IgnoreLineParser.Token.NEGATE); + + // Assert + fail("Expected simple pattern '!' to throw a ParserException."); + } + + @Test + public void parseComment() throws Exception { + // Arrange + final String input = "# This is a comment"; + Part actual = null; + + // Act + List result = IgnoreLineParser.parse(input); + + // Assert + assertEquals(result.size(), 1); + actual = result.get(0); + assertEquals(actual.getToken(), IgnoreLineParser.Token.COMMENT); + assertEquals(actual.getValue(), input); + } + + @Test + public void parseEscapedExclamation() throws Exception { + final String input = "\\!"; + verifyInputToSingleToken(input, IgnoreLineParser.Token.ESCAPED_EXCLAMATION); + } + + @Test + public void parseEscapedSpace() throws Exception { + final String input = "\\ "; + verifyInputToSingleToken(input, IgnoreLineParser.Token.ESCAPED_SPACE); + } + + @Test + public void parseDirectoryMarker() throws Exception { + // Arrange + final String input = "foo/"; + Part actual = null; + + // Act + List result = IgnoreLineParser.parse(input); + + // Assert + assertEquals(result.size(), 2); + actual = result.get(0); + assertEquals(actual.getToken(), IgnoreLineParser.Token.TEXT); + assertEquals(actual.getValue(), "foo"); + actual = result.get(1); + assertEquals(actual.getToken(), IgnoreLineParser.Token.DIRECTORY_MARKER); + } + + @Test + public void parseRooted() throws Exception { + // Arrange + final String input = "/abcd"; + Part actual = null; + + // Act + List result = IgnoreLineParser.parse(input); + + // Assert + assertEquals(result.size(), 2); + actual = result.get(0); + assertEquals(actual.getToken(), IgnoreLineParser.Token.ROOTED_MARKER); + actual = result.get(1); + assertEquals(actual.getToken(), IgnoreLineParser.Token.TEXT); + assertEquals(actual.getValue(), "abcd"); + } + + @Test + public void parseComplex() throws Exception { + // Arrange + final String input = "**/abcd/**/foo/bar/sample.txt"; + Part current = null; + + // Act + Queue result = new LinkedList<>(IgnoreLineParser.parse(input)); + + // Assert + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.MATCH_ALL); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.PATH_DELIM); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.TEXT); + assertEquals(current.getValue(), "abcd"); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.PATH_DELIM); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.MATCH_ALL); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.PATH_DELIM); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.TEXT); + assertEquals(current.getValue(), "foo"); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.PATH_DELIM); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.TEXT); + assertEquals(current.getValue(), "bar"); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.PATH_DELIM); + current = result.remove(); + assertEquals(current.getToken(), IgnoreLineParser.Token.TEXT); + assertEquals(current.getValue(), "sample.txt"); + } + + @Test(expectedExceptions = ParserException.class, + expectedExceptionsMessageRegExp = "The pattern \\*\\*\\* is invalid\\.") + public void parseTripleStarPattern() throws Exception { + // Arrange + final String input = "should/throw/***/anywhere"; + + // Act + List result = IgnoreLineParser.parse(input); + + // Assert + fail("Expected pattern containing '***' to throw a ParserException."); + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/RootedFileRuleTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/RootedFileRuleTest.java new file mode 100644 index 00000000000..471422fcc03 --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/ignore/rules/RootedFileRuleTest.java @@ -0,0 +1,285 @@ +package io.swagger.codegen.ignore.rules; + +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.*; + +public class RootedFileRuleTest { + @Test + public void testMatchFilenameOnly() throws Exception { + // Arrange + final String definition = "/foo"; + final String relativePath = "foo"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testNonMatchFilenameOnly() throws Exception { + // Arrange + final String definition = "/foo"; + final String relativePath = "bar"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } + + @Test + public void testMatchFilenameAndExtension() throws Exception { + // Arrange + final String definition = "/foo.txt"; + final String relativePath = "foo.txt"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo.txt") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testNonMatchFilenameAndExtension() throws Exception { + // Arrange + final String definition = "/foo.txt"; + final String relativePath = "bar.baz"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo.txt") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } + + @Test + public void testMatchFilenameWithGlob() throws Exception { + // Arrange + final String definition = "/foo*"; + final String relativePath = "foobarbaz"; + + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY) + ); + + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testNonMatchFilenameWithGlob() throws Exception { + // Arrange + final String definition = "/foo*"; + final String relativePath = "boobarbaz"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY) + ); + + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } + + @Test + public void testMatchFilenameAndExtensionWithFilenameGlob() throws Exception { + // Arrange + final String definition = "/foo*.txt"; + final String relativePath = "foobarbaz.txt"; + + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".txt") + ); + + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testNonMatchFilenameAndExtensionWithFilenameGlob() throws Exception { + // Arrange + final String definition = "/foo*qux.txt"; + final String relativePath = "foobarbaz.txt"; + + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, "qux.txt") + ); + + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } + + @Test + public void testMatchFilenameAndExtensionWithExtensionGlob() throws Exception { + // Arrange + final String definition = "/foo.*"; + final String relativePath = "foo.bak"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo."), + new Part(IgnoreLineParser.Token.MATCH_ANY) + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testMatchFilenameAndExtensionWithMultiplePeriods() throws Exception { + // Arrange + final String definition = "/foo*.xyzzy.txt"; + final String relativePath = "foo.bar.baz.xyzzy.txt"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".xyzzy.txt") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testNonMatchFilenameAndExtensionWithMultiplePeriods() throws Exception { + // Arrange + final String definition = "/foo*.xyzzy.txt"; + final String relativePath = "foo.bar.baz.qux.txt"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".xyzzy.txt") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } + + @Test + public void testMatchWithoutLeadingForwardSlash() throws Exception { + // Arrange + final String definition = "foo*.xyzzy.txt"; + final String relativePath = "foo.bar.baz.xyzzy.txt"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "foo"), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".xyzzy.txt") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertTrue(actual); + } + + @Test + public void testMatchesOnlyRooted() throws Exception { + // Arrange + final String definition = "/path/to/some/foo*.xyzzy.txt"; + final String relativePath = "foo.bar.baz.xyzzy.txt"; + final List syntax = Arrays.asList( + new Part(IgnoreLineParser.Token.ROOTED_MARKER), + new Part(IgnoreLineParser.Token.TEXT, "path"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.TEXT, "to"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.TEXT, "some"), + new Part(IgnoreLineParser.Token.PATH_DELIM), + new Part(IgnoreLineParser.Token.TEXT, "oo"), + new Part(IgnoreLineParser.Token.MATCH_ANY), + new Part(IgnoreLineParser.Token.TEXT, ".xyzzy.txt") + ); + Rule rule = new RootedFileRule(syntax, definition); + Boolean actual = null; + + // Act + actual = rule.matches(relativePath); + + // Assert + assertFalse(actual); + } +} \ No newline at end of file