package com.perforce.workshop.tjuricek.initializer; import com.esotericsoftware.yamlbeans.YamlReader; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; import com.perforce.workshop.tjuricek.p4java_ext.*; import com.perforce.workshop.tjuricek.initializer.model.*; import com.perforce.p4java.server.IOptionsServer; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import java.io.*; import java.util.*; import java.util.function.Supplier; /** * The Initializer is the entry point for seeding data for Commons * installations. * <p> * It allows you to define data in JSON files (with associated flat files) that * can "replay" operations to a Perforce server or Commons server. The same * information can also be queried (quickly) for test scripts. * <p> * There are two major stages to initialization. The first stage is seeding * the core perforce system. This is where we'll add some basic system users, * for example, that all the other services kind of assume are there. We'll * also set up the rights of these users. For test environments, we'll also * seed a bunch of revisions as well. * <p> * The second stage of initialization actually should be run after the Commons * webserver is available and initialized (and thus, all the commons metadata * exists). Here, we'll run "commons seeding", which will add data from the * perspective of commons; new spaces with files added via file valet, * etc. * <p> * <h2> Specifying Seed Data </h2> * <p> * Data is indicated by creating JSON files and loading them such that a * resource loader can find them either under the path * <code>initializer/perforce</code> or <code>initializer/commons</code>. * <p> * <h2> Executing the Initializer </h2> * <p> * The initializer runs in a single thread, in order. * <p> * The initializer can be run via the command line. It expects that the p4d * instance to seed is already configured. The configuration can be loaded via * a single config file or via overrides. * <p> * Alternatively, the Initializer can be run within your environment. This * expects that the p4java_commons connections respect the user switch provided * by the InitializerConnections interface. */ public class Initializer { private P4Connection p4Connection = null; private ObjectMapper objectMapper; private MustacheFactory mustacheFactory; private Supplier<IOptionsServer> serverSupplier; private ServerWrapper wrapped; public Initializer(Supplier<IOptionsServer> serverSupplier) { objectMapper = new ObjectMapper(); mustacheFactory = new DefaultMustacheFactory(); this.serverSupplier = serverSupplier; } /** * Seeds the perforce data as indicated by the JSON definitions provided * by the stream of "definitions" in the options. * <p> * This will read all definitions, then initialize things in a "rational" * order. The actual algorithm is somewhat tricky and dependent upon * security settings. * <p> * In general, you should indicate definitions in the following order: * <p> * <ol> * <li>Users</li> * <li>Depots</li> * <li>Protections</li> * <li>Triggers</li> * <li>Changelists</li> * </ol> * <p> * You can also indicate clients, if you want a few that "hang around" * after initialization is complete. * <p> * The first user MUST be a superuser. That's the account we try to do most * operations as. You also very likely do not want to perform changelist * operations as the superuser, though you can, in case your usage is * simple. * <p> * In changelist definitions, you can just use the client format * convention "initializer-USER". We'll create that client for use of * seeding data during this function call, and delete it before we're done. * * @param options Options used to configure the function. Required if * you've got triggers and you want to customize the loaded * JSON files. * @throws IllegalStateException if anything bad happens (do not catch, * this means you need to reconfigure your system) */ public void seedPerforce(PerforceOptions options) { PerforceItemSet itemSet = loadPerforceItems(options); // The first user in the declaration should always be the main "super" // account that we do all the user operations as. List<PerforceUser> users = itemSet.getUsers(); if (users.size() == 0) { throw new IllegalStateException("no users are defined, " + "you must have at least one super user to proceed"); } PerforceUser first = users.get(0); if (!first.isSuper()) { throw new IllegalStateException("your first user must be a super " + "user, your test data is likely misconfigured"); } // Create the first user, set the password, then change the connection getServer().setUser(first.getLogin(), null); String superTmpPwd = "superpwd"; String tmpPwd = first.getPassword(); first.setPassword(superTmpPwd); createUser(first); wrapped.setUser(first.getLogin(), first.getPassword()); // Set system configuration // // We *might* want to make this configurable in the future. It's very // unlikely to change in the future, however, and, changing these keys // will affect a lot of logic afterwards. getP4Configure().setSecurity(2); // Hides the keys from normal users // TODO uncomment when we have fixed the bug COMMONS-4605 //p4Configure.setDmKeysHide(2); getP4Passwd().changePasswd(superTmpPwd, tmpPwd); first.setPassword(tmpPwd); // Go through and create all remaining users. users.subList(1, users.size()).forEach(this::createUser); // Create depots itemSet.getDepots().forEach(this::createDepot); // Establish permissions via p4 protects createProtects(users); // Set up triggers that are appropriate for this OS createTriggers(options, itemSet.getTriggers()); // Create any necessary clients Set<PerforceClient> clientSet = new HashSet<PerforceClient>(); clientSet.addAll(itemSet.getClients()); addTemporaryClients(clientSet, itemSet, options); // Now create all clients for (PerforceClient client : clientSet) { createClient(client, itemSet); } // Now go through and create changelists itemSet.getChangelists() .forEach(c -> createChangelist(c, itemSet, clientSet, options)); // We have to reset the login to super, since it's just easier to switch // user context to create clients wrapped.setUser(first.getLogin(), first.getPassword()); // Delete the clients that were really just used to seed changelists removeTemporaryClients(clientSet, itemSet); } private void createUser(PerforceUser user) { P4User p4User = getP4User(); UserSpec userSpec = p4User.load(user.getLogin()); userSpec.setFullName(user.getName()); userSpec.setEmail(user.getEmail()); p4User.forceSave(userSpec); // With security at level 2, we'll need to change the passwd "manually" getP4Passwd().changeUserPasswd(user.getLogin(), null, user.getPassword()); } private P4User getP4User() { return P4User.fromConnection(getP4Connection()); } private ServerWrapper getServer() { if (wrapped == null) { wrapped = new ServerWrapper(serverSupplier.get()); } if (p4Connection == null) { p4Connection = P4Connection.create(wrapped.getOptionsServer()); } return wrapped; } private P4Configure getP4Configure() { return P4Configure.create(getP4Connection()); } private P4Connection getP4Connection() { if (p4Connection == null) { getServer(); } return p4Connection; } /** * Note that this assumes that the wrapped.setUserAndClient was just called. */ private P4ClientConnection getP4ClientConnection() { return P4ClientConnection.create(getP4Connection(), wrapped.getClientName()); } private P4Passwd getP4Passwd() { return P4Passwd.create(getP4Connection()); } /** * Right now we just add protects entries for the super and admin accounts, * and make sure that the guest account just has list access. * <p> * These rules are basic conventions we generally use in Commons-land, * this system isn't to be used to "managed" a real installation. * * @param users The perforce users we create for the Depot. */ private void createProtects(List<PerforceUser> users) { P4Protect p4Protect = P4Protect.fromConnection(getP4Connection()); ProtectSpec spec = p4Protect.load(); spec.clearProtections(); // These are write permissions we've set up in our last OVA // evaluation. Permission depotWrite = new Permission(); depotWrite.setAccessLevel("write"); depotWrite.setUserGroup("user"); depotWrite.setName("*"); depotWrite.setHost("*"); depotWrite.setFiles("//depot/..."); spec.addPermission(depotWrite); Permission commonsUsersWrite = new Permission(); commonsUsersWrite.setAccessLevel("write"); commonsUsersWrite.setUserGroup("user"); commonsUsersWrite.setName("*"); commonsUsersWrite.setHost("*"); commonsUsersWrite.setFiles("//commons/users/..."); spec.addPermission(commonsUsersWrite); for (PerforceUser user : users) { Permission permission = createEntry(user); spec.addPermission(permission); } p4Protect.save(spec); } private Permission createEntry(PerforceUser user) { Permission permission = new Permission(); String accessLevel = "write"; if (user.isSuper()) { accessLevel = "super"; } else if (user.isAdmin()) { accessLevel = "admin"; } else if (user.isGuest()) { accessLevel = "list"; } permission.setAccessLevel(accessLevel); permission.setUserGroup("user"); permission.setName(user.getLogin()); // We may want to set up these users to restrict the host they're // accessing from, though that's easily something the admins can // figure out when setting up a "production" mode permission.setHost("*"); permission.setFiles("//..."); return permission; } private void createDepot(PerforceDepot perforceDepot) { P4Depot p4Depot = P4Depot.create(getP4Connection()); DepotSpec spec = p4Depot.load(perforceDepot.getName()); p4Depot.save(spec); } /** * Reset the triggers table to only use the list of declared triggers. */ private void createTriggers(PerforceOptions options, List<PerforceTrigger> perforceTriggers) { if (perforceTriggers == null || perforceTriggers.isEmpty()) { return; } if (options == null || options.getTriggersDir() == null) { throw new IllegalArgumentException("When seeding triggers, " + "the output triggers directory is required to be " + "configured to the seedPerforce method"); } if (!options.getTriggersDir().exists()) { if (!options.getTriggersDir().mkdirs()) { throw new IllegalStateException("Could not create triggers " + "directory: " + options.getTriggersDir().getAbsolutePath()); } } P4Triggers p4Triggers = P4Triggers.create(getP4Connection()); TriggerSpec triggerSpec = p4Triggers.load(); triggerSpec.clearTriggers(); for (PerforceTrigger perforceTrigger : perforceTriggers) { String script = createTriggerScript(options, perforceTrigger); for (PerforceTrigger.TriggerLine triggerLine : perforceTrigger.getTriggerLines()) { TriggerSpec.Trigger trigger = new TriggerSpec.Trigger(); trigger.setName(triggerLine.getName()); trigger.setType(triggerLine.getType()); trigger.setPath(triggerLine.getPath()); String command = replaceCommand(triggerLine.getCommand(), script); trigger.setCommand(command); triggerSpec.addTrigger(trigger); } } p4Triggers.save(triggerSpec); } private String replaceCommand(String template, String commandPath) { template = template.replace("{_{", "{{"); StringReader reader = new StringReader(template); Mustache mustache = mustacheFactory.compile(reader, commandPath); StringWriter writer = new StringWriter(); CommandContext context = new CommandContext(commandPath); mustache.execute(writer, context); return writer.toString(); } /** * This will look through all changelists we're going to make, and ensure * that each client being referenced exists. If it doesn't exist (by name) * we create a temporary one. * * @param clients The declared client set we expand with temporary clients * @param items The item set with changelists to create */ private void addTemporaryClients(Set<PerforceClient> clients, PerforceItemSet items, PerforceOptions options) { Set<String> names = collectNames(clients); for (PerforceChangelist changelist : items.getChangelists()) { if (!names.contains(changelist.getClient())) { PerforceClient temp = PerforceClient.createTemporaryClient( changelist.getLogin(), options.getClientRootDir() ); clients.add(temp); names.add(temp.getName()); } } } /** * Convert the set to a map indexed by client name. */ private Set<String> collectNames(Set<PerforceClient> clients) { Set<String> names = new HashSet<String>(); for (PerforceClient client : clients) { names.add(client.getName()); } return names; } /** * Creates the client in the system. We do not sync, but we do ensure the * client root directory does exist. * <p> * We log in as the user before loading and creating the client spec. It's * up to the user to reset the context appropriately. * * @param client The client model object * @param items The list of all items */ private void createClient(PerforceClient client, PerforceItemSet items) { PerforceUser user = items.getUser(client.getOwner()); if (user == null) { throw new IllegalStateException("client does not reference a defined" + "user: " + client.toString()); } wrapped.setUser(user.getLogin(), user.getPassword()); P4Client p4Client = P4Client.create(getP4Connection()); ClientSpec clientSpec = p4Client.load(client.getName()); clientSpec.setRoot(client.getRootFile().getAbsolutePath()); if (client.isDescriptionEdit()) { clientSpec.setDescription(client.getDescription()); } if (client.isHostEdit()) { clientSpec.setHost(client.getHost()); } p4Client.save(clientSpec); if (!client.getRootFile().exists()) { if (!client.getRootFile().mkdirs()) { throw new IllegalStateException("unable to create client root: " + client.getRootFile().getAbsolutePath()); } } } /** * Remove any client marked as "temporary". We don't want them lingering * around pointing to a temp directory no longer in use on this machine. * * @param clients The set of PerforceClients we used to seed all initial * changelists */ private void removeTemporaryClients(Set<PerforceClient> clients, PerforceItemSet items) { P4Client p4Client = P4Client.create(getP4Connection()); for (PerforceClient client : clients) { if (client.isTemporary()) { p4Client.delete(client.getName()); if (client.getRootFile().exists()) { try { FileUtils.deleteDirectory(client.getRootFile()); } catch (IOException e) { throw new IllegalStateException(e); } } } } } private void createChangelist(PerforceChangelist changelist, PerforceItemSet items, Set<PerforceClient> clients, PerforceOptions options) { PerforceUser user = items.getUser(changelist.getLogin()); wrapped.setUserAndClient(user.getLogin(), user.getPassword(), changelist.getClient()); P4ClientConnection clientConnection = getP4ClientConnection(); P4Change p4Change = P4Change.create(clientConnection); // Create pending changelist ChangeSpec changeSpec = p4Change.load(null); changeSpec.setStatus("pending"); if (changelist.getDescription() != null) { changeSpec.setDescription(changelist.getDescription()); } p4Change.update(changeSpec); PerforceClient client = findClientByName(clients, changelist.getClient()); if (client == null) { throw new IllegalStateException("no client registered with name " + changelist.getClient()); } P4Add p4Add = P4Add.create(clientConnection); // mark adds changelist.getAdds().forEach(add -> { String localPath = unpackResource(add.getResource(), add.getClientPath(), client, options); p4Add.add(changeSpec.getChange(), null, localPath); }); // mark edits P4Edit p4Edit = P4Edit.create(clientConnection); for (PerforceEdit edit : changelist.getEdits()) { File localFile = new File(client.getRootFile(), edit.getClientPath()); p4Edit.edit(changeSpec.getChange(), localFile.getAbsolutePath()); unpackResource(edit.getResource(), edit.getClientPath(), client, options); } // Submit changelist P4Submit p4Submit = P4Submit.create(clientConnection); p4Submit.submit(changeSpec.getChange()); } private String unpackResource(String resourcePath, String clientPath, PerforceClient perforceClient, PerforceOptions options) { String localPath = null; InputStream in = null; OutputStream out = null; try { in = options.getInputStreamLoader().getInputStream(resourcePath); File outputFile = new File(perforceClient.getRootFile(), clientPath); localPath = outputFile.getAbsolutePath(); if (!outputFile.getParentFile().exists()) { if (!outputFile.getParentFile().mkdirs()) { throw new IllegalStateException("could not create directory " + outputFile.getParentFile().getAbsolutePath()); } } out = new FileOutputStream(outputFile); IOUtils.copy(in, out); return localPath; } catch (FileNotFoundException e) { throw new IllegalStateException(e); } catch (IOException e) { throw new IllegalStateException(e); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(out); } } private PerforceClient findClientByName(Set<PerforceClient> clients, String name) { for (PerforceClient client : clients) { if (name.equals(client.getName())) { return client; } } return null; } /** * Unloads the correct template file, running it through mustache if the * extension ends with ".mustache". * <p> * We ensure the script is executable. * * @param perforceOptions The seed options: we use the context for the * template being rendered * @param perforceTrigger The kind of trigger we want to supply * @return The absolute path of the trigger we just created. * @throws java.lang.IllegalStateException in the face of problems */ private String createTriggerScript(PerforceOptions perforceOptions, PerforceTrigger perforceTrigger) { String resourcePath = null; if (SystemUtils.IS_OS_WINDOWS) { resourcePath = perforceTrigger.getBatch(); } else if (SystemUtils.IS_OS_UNIX) { resourcePath = perforceTrigger.getBash(); } if (resourcePath == null) { throw new IllegalStateException("Did not find the trigger script " + "for this OS from " + perforceTrigger.toString()); } boolean isMustache = false; File resourcePathFile = new File(resourcePath); String resourceName = resourcePathFile.getName(); if (resourceName.endsWith(".mustache")) { resourceName = StringUtils.removeEnd(resourceName, ".mustache"); isMustache = true; } InputStream inputStream = ClassLoader.getSystemResourceAsStream(resourcePath); BufferedInputStream buffer; File outputFile = new File(perforceOptions.getTriggersDir(), resourceName); FileWriter outputWriter = null; try { buffer = new BufferedInputStream(inputStream); outputWriter = new FileWriter(outputFile); if (isMustache) { InputStreamReader reader = new InputStreamReader(buffer); Mustache template = mustacheFactory.compile(reader, resourcePath); template.execute(outputWriter, perforceOptions.getTemplateContext()); } else { IOUtils.copy(buffer, outputWriter); } } catch (IOException e) { throw new IllegalStateException("could not read resource: " + resourcePath); } finally { IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(outputWriter); } if (!outputFile.canExecute()) { outputFile.setExecutable(true); } return outputFile.getAbsolutePath(); } /** * Load all json files under initializer/perforce/.json. * * @return The PerforceItemSet ready for initialization * @throws IllegalStateException When something bad happens; any error * resolution is simply going to require * system configuration * @options options The PerforceOptions that we use to load most of the * definitions from. */ public PerforceItemSet loadPerforceItems(PerforceOptions options) { PerforceItemSet items = new PerforceItemSet(); Iterator<InputStream> defs = options.getDefinitions(); while (defs.hasNext()) { BufferedInputStream buffer = null; try { buffer = new BufferedInputStream(defs.next()); PerforceItem item = objectMapper.readValue(buffer, PerforceItem.class); items.add(item); } catch (JsonMappingException e) { throw new IllegalStateException(e); } catch (JsonParseException e) { throw new IllegalStateException(e); } catch (IOException e) { throw new IllegalStateException(e); } finally { IOUtils.closeQuietly(buffer); } } return items; } /** * We push the loading of resources to the application, just because in * some cases the application can offer the ability to load from the * classpath... or from a local directory. That configuration is not to * be decided by this library. */ public static interface InputStreamLoader { InputStream getInputStream(String resourceName); } /** * Command line, which basically just takes a single configuration file * (because this is just that complicated). We generate a configuration file * template just to make things a little easier. * * @param args The command line strings, should really either be "-h" or * the path of a YAML configuration file. */ public static void main(String[] args) { List<String> argList = new ArrayList<>(); Collections.addAll(argList, args); boolean showUsage = false; if (argList.isEmpty()) { showUsage = true; } else if (argList.stream() .filter(a -> a.startsWith("-h") || a.startsWith("--help")) .findFirst().isPresent()) { showUsage = true; } if (showUsage) { System.err.println("Usage: java -jar initializer.jar config.yml"); System.err.println("\nOptions:"); System.err.println(" - config.yml: A Yaml configuration file containing the necessary"); System.err.println(" information for the initializer to run."); return; } File configFile = new File(argList.get(0)); if (!configFile.exists()) { System.err.println("Configuration file does not exist: " + configFile.toString()); System.exit(1); } FileReader fileReader = null; try { fileReader = new FileReader(configFile); YamlReader yamlReader = new YamlReader(fileReader); InitializerConfig config = yamlReader.read(InitializerConfig.class); P4Connection conn = P4Connection.create( config.getConnectionSettings().getHostname(), config.getConnectionSettings().getPort(), config.getConnectionSettings().isSsl(), "Initializer", "0.1.0" ); Initializer initializer = new Initializer( () -> conn.getOptionsServer() ); initializer.seedPerforce(config.getOptions()); } catch (Exception e) { System.err.println("Initializer failed. I'm lazy, here's a stack trace."); e.printStackTrace(); System.exit(1); } finally { IOUtils.closeQuietly(fileReader); } } /** * Defines configuration values that are mapped via YAML code into this * class. */ public static class InitializerConfig { private ConnectionSettings connectionSettings; private PerforceOptions options; public ConnectionSettings getConnectionSettings() { return connectionSettings; } public void setConnectionSettings(ConnectionSettings connectionSettings) { this.connectionSettings = connectionSettings; } public PerforceOptions getOptions() { return options; } public void setOptions(PerforceOptions options) { this.options = options; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InitializerConfig that = (InitializerConfig) o; if (connectionSettings != null ? !connectionSettings.equals(that.connectionSettings) : that.connectionSettings != null) return false; if (options != null ? !options.equals(that.options) : that.options != null) return false; return true; } @Override public int hashCode() { int result = connectionSettings != null ? connectionSettings.hashCode() : 0; result = 31 * result + (options != null ? options.hashCode() : 0); return result; } @Override public String toString() { return "InitializerConfig{" + "connectionSettings=" + connectionSettings + ", options=" + options + '}'; } } /** * Defines how we generally will connect to the p4d server we're * initializing. Since the first user in our JSON data is supposed to be * our new super user, there's not a lot to configure here. */ private static class ConnectionSettings { private String hostname = "localhost"; private int port = 1666; private boolean ssl = false; /** * Defaults to 'localhost' */ public String getHostname() { return hostname; } public void setHostname(String hostname) { this.hostname = hostname; } /** * Defaults to 1666 */ public int getPort() { return port; } public void setPort(int port) { this.port = port; } /** * Defaults to false. * * This has not been tested. */ public boolean isSsl() { return ssl; } public void setSsl(boolean ssl) { this.ssl = ssl; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ConnectionSettings that = (ConnectionSettings) o; if (port != that.port) return false; if (ssl != that.ssl) return false; if (hostname != null ? !hostname.equals(that.hostname) : that.hostname != null) return false; return true; } @Override public int hashCode() { int result = hostname != null ? hostname.hashCode() : 0; result = 31 * result + port; result = 31 * result + (ssl ? 1 : 0); return result; } @Override public String toString() { return "ConnectionSettings{" + "hostname='" + hostname + '\'' + ", port=" + port + ", ssl=" + ssl + '}'; } } /** * Options to configure the seedPerforce method. The definitions and * client root directory are required. */ public static class PerforceOptions { private Object templateContext; private File triggersDir; private File clientRootDir; private Iterator<InputStream> definitions; private InputStreamLoader inputStreamLoader; /** * If we discover a file name (like a trigger script) with the extension * ".mustache", we'll treat it like a Mustache template, and use this * context to render the file. That way you can pass some app config * on down through to things like trigger scripts. */ public Object getTemplateContext() { return templateContext; } public void setTemplateContext(Object templateContext) { this.templateContext = templateContext; } /** * When the system appends triggers, we typically have script files, * which get put in to this directory. */ public File getTriggersDir() { return triggersDir; } public void setTriggersDir(File triggersDir) { this.triggersDir = triggersDir; } /** * The root directory for all client workspaces. Most of these are * expected to be temporary, and cleaned up as initialization finishes. */ public File getClientRootDir() { return clientRootDir; } public void setClientRootDir(File clientRootDir) { this.clientRootDir = clientRootDir; } /** * Load the JSON definitions that are then converted into steps for the * initializer. */ public Iterator<InputStream> getDefinitions() { return definitions; } public void setDefinitions(Iterator<InputStream> definitions) { this.definitions = definitions; } public InputStreamLoader getInputStreamLoader() { return inputStreamLoader; } public void setInputStreamLoader(InputStreamLoader inputStreamLoader) { this.inputStreamLoader = inputStreamLoader; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PerforceOptions that = (PerforceOptions) o; if (clientRootDir != null ? !clientRootDir.equals(that.clientRootDir) : that.clientRootDir != null) return false; if (definitions != null ? !definitions.equals(that.definitions) : that.definitions != null) return false; if (inputStreamLoader != null ? !inputStreamLoader.equals(that.inputStreamLoader) : that.inputStreamLoader != null) return false; if (templateContext != null ? !templateContext.equals(that.templateContext) : that.templateContext != null) return false; if (triggersDir != null ? !triggersDir.equals(that.triggersDir) : that.triggersDir != null) return false; return true; } @Override public int hashCode() { int result = templateContext != null ? templateContext.hashCode() : 0; result = 31 * result + (triggersDir != null ? triggersDir.hashCode() : 0); result = 31 * result + (clientRootDir != null ? clientRootDir.hashCode() : 0); result = 31 * result + (definitions != null ? definitions.hashCode() : 0); result = 31 * result + (inputStreamLoader != null ? inputStreamLoader.hashCode() : 0); return result; } @Override public String toString() { return "PerforceOptions{" + "templateContext=" + templateContext + ", triggersDir=" + triggersDir + ", clientRootDir=" + clientRootDir + ", definitions=" + definitions + ", inputStreamLoader=" + inputStreamLoader + '}'; } } private static class CommandContext { private String command; private CommandContext(String command) { this.command = command; } public String getCommand() { return command; } } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#2 | 9172 | tjuricek | 0.1.2 Added command line execution options and documentation to run the initializer locally. | ||
#1 | 9165 | tjuricek |
Some documentation framework and a fatjar style distribution for the initializer to run outside of a gradle plugin. Upped the version to 0.1.2, but this may need some testing before it's really guaranteed to work in standalone mode |
||
//guest/tjuricek/initializer/src/main/java/com/perforce/workspace/tjuricek/initializer/Initializer.java | |||||
#1 | 9088 | tjuricek |
0.1.0 First version of the initializer. This currently requires setting up a local version of the p4java_ext project, since that project is awating approval into the main jcenter repository. |