View Javadoc
1   package emissary.command;
2   
3   import emissary.command.converter.PathExistsConverter;
4   import emissary.command.converter.ProjectBaseConverter;
5   import emissary.config.ConfigUtil;
6   import emissary.core.EmissaryException;
7   
8   import ch.qos.logback.classic.ClassicConstants;
9   import ch.qos.logback.classic.LoggerContext;
10  import ch.qos.logback.classic.util.ContextInitializer;
11  import ch.qos.logback.core.joran.spi.JoranException;
12  import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
13  import jakarta.annotation.Nullable;
14  import org.slf4j.Logger;
15  import org.slf4j.LoggerFactory;
16  import picocli.CommandLine;
17  import picocli.CommandLine.Command;
18  import picocli.CommandLine.Option;
19  import picocli.CommandLine.ParameterException;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.PrintStream;
23  import java.net.URL;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.List;
28  
29  @Command(description = "Base Command")
30  public abstract class BaseCommand implements EmissaryCommand {
31      static final Logger LOG = LoggerFactory.getLogger(BaseCommand.class);
32  
33      public static final String COMMAND_NAME = "BaseCommand";
34  
35      @Option(names = {"-c", "--config"}, description = "config dir, comma separated if multiple, defaults to <projectBase>/config",
36              converter = PathExistsConverter.class)
37      @Nullable
38      private Path config;
39  
40      @Option(names = {"-b", "--projectBase"}, description = "defaults to PROJECT_BASE, errors if different\nDefault: ${DEFAULT-VALUE}",
41              converter = ProjectBaseConverter.class)
42      private Path projectBase = Paths.get(System.getenv("PROJECT_BASE"));
43  
44      @Option(names = "--logbackConfig", description = "logback configuration file, defaults to <configDir>/logback.xml")
45      @Nullable
46      private String logbackConfig;
47  
48      @Option(names = {"--flavor"}, description = "emissary config flavor, comma separated for multiple")
49      private String flavor;
50  
51      @Option(names = {"--binDir"}, description = "emissary bin dir, defaults to <projectBase>/bin")
52      @Nullable
53      private Path binDir;
54  
55      @Option(names = {"--outputRoot"}, description = "root output directory, defaults to <projectBase>/localoutput")
56      @Nullable
57      private Path outputDir;
58  
59      @Option(names = {"--errorRoot"}, description = "root error directory, defaults to <projectBase>/localerrors")
60      @Nullable
61      private Path errorDir;
62  
63      @Option(names = {"-q", "--quiet"}, description = "hide banner and non essential messages\nDefault: ${DEFAULT-VALUE}")
64      private boolean quiet = false;
65  
66      public Path getConfig() {
67          if (config == null) {
68              config = getProjectBase().toAbsolutePath().resolve("config");
69              if (!Files.exists(config)) {
70                  throw new IllegalArgumentException("Config dir not configured and " + config.toAbsolutePath() + " does not exist");
71              }
72          }
73  
74          return config;
75      }
76  
77      public Path getProjectBase() {
78          return projectBase;
79      }
80  
81      public String getLogbackConfig() {
82          if (logbackConfig == null) {
83              return getConfig() + "/logback.xml";
84          }
85          return logbackConfig;
86      }
87  
88      public String getFlavor() {
89          return flavor;
90      }
91  
92      protected void overrideFlavor(String flavor) {
93          logInfo("Overriding current {} {} to {} ", ConfigUtil.CONFIG_FLAVOR_PROPERTY, getFlavor(), flavor);
94          this.flavor = flavor;
95          System.setProperty(ConfigUtil.CONFIG_FLAVOR_PROPERTY, getFlavor());
96      }
97  
98      public Path getBinDir() {
99          if (binDir == null) {
100             return getProjectBase().toAbsolutePath().resolve("bin");
101         }
102         return binDir;
103     }
104 
105     public Path getOutputDir() {
106         if (outputDir == null) {
107             return getProjectBase().toAbsolutePath().resolve("localoutput");
108         }
109         return outputDir;
110     }
111 
112     public Path getErrorDir() {
113         if (errorDir == null) {
114             return getProjectBase().toAbsolutePath().resolve("localerror");
115         }
116         return errorDir;
117     }
118 
119     public boolean getQuiet() {
120         return quiet;
121     }
122 
123     public boolean isVerbose() {
124         return !getQuiet();
125     }
126 
127     @Override
128     public void setupCommand() {
129         setupConfig();
130     }
131 
132     public void setupConfig() {
133         logInfo("{} is set to {} ", ConfigUtil.PROJECT_BASE_ENV, getProjectBase().toAbsolutePath().toString());
134         setSystemProperty(ConfigUtil.CONFIG_DIR_PROPERTY, getConfig().toAbsolutePath().toString());
135         setSystemProperty(ConfigUtil.CONFIG_BIN_PROPERTY, getBinDir().toAbsolutePath().toString());
136         setSystemProperty(ConfigUtil.CONFIG_OUTPUT_ROOT_PROPERTY, getOutputDir().toAbsolutePath().toString());
137         logInfo("Emissary error dir set to {} ", getErrorDir().toAbsolutePath().toString());
138         if (getFlavor() != null) {
139             setSystemProperty(ConfigUtil.CONFIG_FLAVOR_PROPERTY, getFlavor());
140         }
141     }
142 
143     protected void setSystemProperty(String key, String value) {
144         logInfo("Setting {} to {} ", key, value);
145         System.setProperty(key, value);
146     }
147 
148     /**
149      * Create a new command and parse the args.
150      * <p>
151      * Useful for testings. Also calls setup so properties are set
152      * 
153      * @param clazz the Class of return type class
154      * @param args vararg of Strings
155      */
156     @SuppressWarnings("SystemOut")
157     public static <T extends EmissaryCommand> T parse(Class<T> clazz, String... args) throws EmissaryException {
158         T cmd;
159         try {
160             cmd = clazz.cast(Class.forName(clazz.getName()).getDeclaredConstructor().newInstance());
161         } catch (ReflectiveOperationException e) {
162             throw new EmissaryException("Cannot construct command", e);
163         }
164         ByteArrayOutputStream baos = new ByteArrayOutputStream();
165         PrintStream ps = new PrintStream(baos);
166         PrintStream old = System.out;
167         System.setOut(ps);
168 
169         CommandLine cl;
170         int code;
171         try {
172             cl = new CommandLine(cmd);
173             code = cl.execute(args);
174         } finally {
175             System.out.flush();
176             System.setOut(old);
177         }
178         if (cl != null && code == 2) {
179             throw new ParameterException(cl, baos.toString());
180         }
181         cmd.setup();
182         return cmd;
183     }
184 
185     /**
186      * Create a new command and parse the args
187      * <p>
188      * Useful for testings
189      * 
190      * @param clazz the Class of return type class
191      * @param args vararg of Strings
192      */
193     public static <T extends EmissaryCommand> T parse(Class<T> clazz, List<String> args) throws EmissaryException {
194         return parse(clazz, args.toArray(new String[0]));
195     }
196 
197     /*
198      * Try to reinitialize the logback context with the configured file you may have 2 log files if anything logged before
199      * we do this. Useful when you are running a server For troubleshooting, looking at the http://localhost:8001/lbConfig
200      * when this works, you will see the initial logger and then new one
201      *
202      * Need to reinit because logback config uses ${emissary.node.name}-${emissary.node.port} which are now set by the
203      * commands after logback is initialized
204      */
205     public void reinitLogback() {
206         LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
207         URL logCfg = ConfigurationWatchListUtil.getMainWatchURL(loggerContext);
208 
209         if (logCfg != null && (logCfg.toString().endsWith("logback-test.xml") || logCfg.toString().endsWith("logback-test.groovy"))) {
210             // logCfg can be null if Emissary.setupLogbackForConsole is called
211             LOG.warn("Not using {}, staying with test config {}", getLogbackConfig(), logCfg);
212             doLogbackReinit(loggerContext, logCfg.getPath());
213         } else if (Files.exists(Paths.get(getLogbackConfig()))) {
214             doLogbackReinit(loggerContext, getLogbackConfig());
215         } else {
216             LOG.warn("logback configuration not found {}, not reconfiguring logging", getLogbackConfig());
217         }
218 
219     }
220 
221     private void doLogbackReinit(LoggerContext loggerContext, String configFilePath) {
222         System.setProperty(ClassicConstants.CONFIG_FILE_PROPERTY, configFilePath);
223         loggerContext.reset();
224         ContextInitializer newContext = new ContextInitializer(loggerContext);
225         try {
226             newContext.autoConfig();
227         } catch (JoranException e) {
228             LOG.error("Problem reconfiguring logback with {}", getLogbackConfig(), e);
229         }
230     }
231 
232     public void logInfo(String format, Object... args) {
233         if (isVerbose()) {
234             LOG.info(format, args);
235         }
236     }
237 
238     @Override
239     public void outputBanner() {
240         if (isVerbose()) {
241             new Banner().dump();
242         }
243     }
244 
245     @Override
246     public void run() {}
247 }