All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javax0.geci.api.Source Maven / Gradle / Ivy

package javax0.geci.api;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

/**
 * A {@code Source} represents a source file in the project that the
 * generator can modify. To do that there are methods that support the
 * modification. Also there are static methods that support the
 * definition of where the source files are.
 *
 * 

The source file contains lines. There are certain parts in a * source file that are represented as segments. For example in the Java * code the lines that are between *

 *     {@code
 *     //
 *     //
 *     }
 * 
*

* contain a segment (excluding the surrounding lines that signal the * start and the end of the segment. The {@code Source} object can be * asked to provide a {@link Segment} object that represents those * lines. Segments can not be read. They are there to be written and * when a segment is written the lines written to the segment will * replace the original lines. Every segment has an identifier, in the * example above this is {@code segmentIdentifier}. */ public interface Source { /** * This method can be used to specify that the directories are * standard maven directories. When the project is multi-module and * the test code invoking Geci is not in the same module as the code * needing to be processed by the generator then declaration of * sources should use this method. The argument should be '{@code * ..}' or whatever the directory of the root module is relative to * the directory of the module where the test is. * * @param root the directory to the root module where the top level * {@code pom.xml} containing the *

     *             {@code 
     *                 ...
     *               }
     *             
* declaration is. * @return a new Maven source directory configuration object. */ static Maven maven(final String root) { return new Maven(root); } /** * Simple method to specify that the sources are in a maven project * and they follow the standard directory structure. * * @return a new Maven source directory configuration object. */ static Maven maven() { return new Maven(); } /** *

Return the named segment that the generator can write. Return * {@code null} if there is no such segment in the file.

* *

The global segment cannot be opened while the lines of the segments * are borrowed. See {@link #borrows()} and {@link #returns(List)}

* * @param id the name of the segment as defined in the * {@code id="..."} xml tag of the * {@code } * @return the segment or {@code null}. * @throws IOException in case there is no file or file is not * readable */ Segment open(String id) throws IOException; /** * Segments start and ends signalled with specific lines. In some * cases the code generator allows the source object to invent a * nonexistent segment. In that case this is called the default * segment. When the default segment is handled the segment start * and end pre and postfixes are inserted into the segment. * *

Because this is a kind of dangerous feature this is only * performed when the generator allows it explicitly. * *

There is no method to set it false, but the framework resets * this flag before invocation of every generator {@link * Generator#process(Source)}. */ void allowDefaultSegment(); /** * Same as {@link #open(String)} but if the segment cannot be found then it * throws a Geci exception instead of returning {@code null} * * @param id the name of the segment as defined in the {@code id="..."} xml tag of the {@code } * @return the segment and never {@code null} * @throws IOException in case there is no file or file is not readable */ Segment safeOpen(String id) throws IOException; /** * Create a temporary segment. This segment is dangling and does not * belong to the actual source and also it does not have a name. The * starting tab stop of the segment is zero. Extra padding is added * to the lines when the segment is appended to the final segment. * As it implies such temporary segments can be used to write code * into this segment and later merge this segment into one segment * that belongs to a source. * * @return a new anonymous segment */ Segment temporary(); /** *

Open the "global" segment that is the whole source file. Usually used on sources that are created calling * {@link #newSource(String)}

*

If you invoke this method on a source that was "hand-made" then you will essentially delete the content of the * file. If you want to get the content of a source file you should call {@link #getLines()} on the source object * itself.

*

The global segment cannot be opened while the lines of the segments * are borrowed. See {@link #borrows()} and {@link #returns(List)}

* * @return the new segment object. */ Segment open(); /** * Get all the segment names that are defined in the source * * @return the set of the names of the segments */ java.util.Set segmentNames(); /** *

Generators can use this method to read the whole content of a file. The content of the list should not be * modified and should be treated as immutable.

*

The lines cannot be read while they are borrowed. See {@link #borrows()} and {@link #returns(List)}

* * @return the list of the strings that contain the lines of the source file. */ List getLines(); /** *

A generator may decide to use the lines of the source as they * are without the help of the Source object. To signal this * intention it should borrow the lines from the Source. While the * lines are borrowed the Source cannot be modified in any way * calling API that modifies the Source object.

* *

When the generator is ready with the modifications then it has * to return the possibly modified line list calling {@link * #returns(List)}

* *

A generator cannot call {@code borrows()} if the lines were * already borrowed.

* * @return the list of the lines */ default List borrows() { return getLines(); } /** *

When a generator borrowed the source line (see {@link #borrows()} from the Source then it also has to * returns the lines before it finishes its work.

*

It is possible to tell the Source object that the lines were not modified. In this case the argument * has to be {@code null}

* * @param lines the list of the lines, or {@code null} in case the lines, as they are in the Source object are OK */ void returns(List lines); /** * Get the absolute file name of this source. * * @return the absolute file name of the source */ String getAbsoluteFile(); /** * Create a new source that may or may not exist. This source is going to be generated and if it already existed * it will be overwritten, unless the already existing file has exactly the same content as the new one. * * @param fileName relative file name to the current source. * @return the new {@code Source} object. */ Source newSource(String fileName); /** * Create a new source that may or may not exist. This source is going to be generated and if it already existed * it will be overwritten, unless the already existing file has exactly the same content as the new one. * Create the new source file in the directory that was used to open the specified source set. * * @param fileName relative file name to the current source. * @param set identifies the source set with a name * @return the new {@code Source} object. */ Source newSource(Source.Set set, String fileName); /** * Initialize a segment. This is needed in case when the code generator does not * write anything into the segment. In that case the segment may not even be opened and in that * case the segment is not touched and the old text, presumably garbage may be there. For example, * you have a setter/getter generated code that selects some of the fields only, say only the * 'protected' fields. During the development you change the last 'protected' field to private * and there remains no field for which setter and getter is to be generated. If the segment is * not init-ed, then the code generator would not touch the segment and it may contain the * setter and the getter for the last 'protected' by now 'private' field. *

* Technically calling init is similar to opening a segment, though init should be more lenient * to opening non-existent segments. * * @param id the identifier of the segment. May be {@code null}. * @throws IOException in case there is no file or file is not readable */ void init(String id) throws IOException; /** * Get the name of the class that corresponds to this source. The class may not exist though. *

* When calculating the class name the directory structure is used and the name of the source file * chopping off the {@code .java} or other extension. *

* Note: The seemingly weird decision to use 'K' in the name of the method provides some javax0.geci.consistency. The method * {@link #getKlass()} cannot be named {@code getClass()} because that would override the method of the same name * in the class {@code Object}. Based on that all the methods that are returning the name or simple name of the * "klass" uses the letter 'K' even though these two methods could be named with the letter 'C'. * * @return the class name that was calculated from the file name */ String getKlassName(); /** * Get the calculated class simple name. That is the name of the class without the package name in front of it. * * @return the simple name of the class */ String getKlassSimpleName(); /** * Get the name of the class where the class corresponding to the source is or should be. * * @return the name of the package */ String getPackageName(); /** * Get the class that corresponds to this source. If the class does not exists, either because the * source file is not a Java source or because the file was not compiled or just for any reason then * the method returns {@code null}. * * @return the class object or {@code null} */ Class getKlass(); /** * Get a logger that the generator can use to send messages to the * logging system. It is recommended to use this logger in the * generator and not the system or other loggers as this logger may * be implemented in a way that it collects the messages of the * different generators and send it to the log target together * putting the messages that are related to the same source * together. * * @return the logger for the generator */ Logger getLogger(); /** *

Set serves as an identifier class for a source set. Essentially {@code Set} is nothing else but a string * encapsulated in the class. The constructor and the methods help the definition and the generation of this * identifying string.

* * *

This class is used, for example, when a source is asked to open a new source that does not exist yet and the new * source should be created in the directory tree of a different source set. In this case the generator is calling * {@link #newSource(Set, String)} specifying the set creating a new {@code Set()} object with the name of the * source set.

* *

When a source set is not identified, for example it is specified calling the method {@link * Geci#source(String...)} that only specifies the alternative directories where the source files can be, then the * source set will have a {@code new Set("xxx")} object as identifier. The identifier {@code "xxx"} in this case is * a decimal number string and it is randomly created.

* *

When a generator needs to name a set it has to be identified and a specific first argument has to be passed to * the {@link Geci#source(Set, String...)} usually naming the source set typically as {@code set("java")} or {@code * set("resources")}.

*/ class Set { private String name; private final boolean autoName; /** *

Create a new source set object.

* *

The name can be {@code null}. In that case the name will be created automatically creating a random * number. The caller should also specify that the name that was passed in the first argument is something that * comes from configuration from the user code or it was automatically generated. In the latter case the second * argument has to be {@code true}.

* *

It is an error to state that the name was created automatically {@code autoName==true} and providing * a {@code null} value as a {@code name}. Such a situation is evidently a programming error in the caller * and as such should fail fast.

* *

Note that this constructor is {@code private} but there are {@code public} {@code static} methods in the * class that expose this functionality to the outside word. Also note that the class itself is package access, * which means that code out of the package should treat instances as opaque ID classes only to pass the * reference as parameter to classes in this package.

* * @param name the name of the set or {@code null} in case there was no name specified. In this case a * random name (a decimal number) will be assigned to the set as a string identifier. * @param autoName signals if the name was created automatically. It is NOT a request to name the set * automatically. That is done in case {@code name == null}. */ private Set(String name, boolean autoName) { if (name == null && autoName) { throw new GeciException("When the name for a set is not specified it cannot be 'autoName'"); } if (name == null) { name = randomUniqueName(); } this.name = name; this.autoName = autoName; } /** * Create a random identifier. For the shake of simplicity and to avoid identifier collision the ID string * created is the {@link Object#hashCode()} value in decimal format of this object. * * @return the random identifier string */ private String randomUniqueName() { return "" + super.hashCode(); } /** * When a set was automatically named, like {@code mainSource} then it may happen that there are more than * one sets with that name. It will cause collision in the directory map. In that case we try to rename the * set. If the name was specified by the user, from top level and not automatically named then it is an error. * The programmer using the package must not name two different sets with the same name. On the other hand when * the sets were getting their default name, then it is a safe routine to rename them. */ public void tryRename() { if (autoName) { name = randomUniqueName(); } } /** *

Create a {@code Source} set identifying object with the given name.

* *

Import this method as a static import into the generators and into the test code that invokes the code * generator.

* * @param name identifying string of the source set * @return a new identifier object. */ public static Set set(String name) { return new Set(name, false); } /** * Crete a new set with the name {@code name}. The argument * {@code autoName} signals that the name is a default name and * it is not directly specified by the user. This is like {@code * mainSource} or {@code testResource}. In this case it may * happen that there are multiple sets with the same name * especially if multiple modules are specified as sources in a * multi-module project. If that happens when the application * tries to save the source into the map indexed by the source * set objects it renames the set with a "random" name. * *

Note that the "random" name is also used when the user * defines a set without name. In that case the argument {@code * name} is {@code null} and {@code autoName} has to be {@code * false}. * * @param name the name of the set * @param autoName signals if the name was set automatically * @return the new set */ public static Set set(String name, boolean autoName) { return new Set(name, autoName); } public static Set set() { return set(null); } @Override public String toString() { return name; } @Override public boolean equals(Object that) { if (this == that) return true; if (!(that instanceof Set)) { return false; } Set set = (Set) that; return Objects.equals(name, set.name); } @Override public int hashCode() { return Objects.hash(name); } } class NamedSourceSet { final Set set; final String[] directories; NamedSourceSet(Set set, String[] directories) { this.set = set; this.directories = directories; } } /** * This class provides predicates that can be used as an argument * to the methods {@link Geci#source(Predicate, String...)} and * {@link Geci#source(Set, Predicate, String...)}. */ class Predicates { /** * Creates the default predicate that tests {@code true} if the * directory exists and is a directory. * * @return the predicate. */ public static Predicate exists() { return file -> new File(file).isDirectory(); } /** * Returns a predicate that tests {@code true} if the file * exists, it is a directory and a file with the anchor name * can be found in the directory. The anchor name may contain * leading directory names that make it relative to the tested * directory. * * @param anchor the anchor file. * @return the predicate */ public static Predicate hasTheFile(String anchor) { return exists().and(file -> new File(file + anchor).exists()); } /** * Returns a predicate that tests {@code true} if the file * exists, it is a directory and a one of the files with the * anchor names can be found in the directory. The anchor name * may contain leading directory names that make it relative to * the tested directory. * * @param anchors the anchor files one of which should be there * in the directory. * @return the predicate */ public static Predicate hasOneOfTheFiles(String... anchors) { return exists().and(file -> Arrays.stream(anchors).anyMatch(anchor -> new File(file + anchor).exists())); } /** * Returns a predicate that tests {@code true} if the file * exists, it is a directory and a all of the files with the * anchor names can be found in the directory. The anchor name * may contain leading directory names that make it relative to * the tested directory. * * @param anchors the anchor files which all should be there in * the directory. * @return the predicate */ public static Predicate hasAllTheFiles(String... anchors) { return exists().and(file -> Arrays.stream(anchors).allMatch(anchor -> new File(file + anchor).exists())); } } /** * Class to build up the directory structures that correspond to the * Maven directory structure */ class Maven { private final String rootModuleDir; private String module = null; private Maven() { rootModuleDir = null; } private Maven(final String root) { rootModuleDir = root; } /** * Return the array of directories where the Java sources could * be found. * *

If there is no maven module defined then there is only * one directory, * *

         * '{@code ./src/(main|test)/(java|resources)}'
         * 
*

* where the sources can be. This is sufficient. The current * working directory is the project root when the tests are * started either interactively in an IDE or from the command * line using the command {@code mvn}. * *

If there is a maven module defined then execution of the * tests and thus the generators can be started with different * current working directories. Practice shows that the current * working directory is the project root when you start the code * generation along with the test using the {@code mvn} command. * On the other hand when the tests are executed from the IDE * then the current working directory is the root directory of * the module root where the test belongs to. * *

When the test executing the code generation is in the * same module as the code that is the target of the code * generation then the directories are * *

         *     {@code ./module/src/(main|test)/(java|resources)
         *     }
         * 
*

* or * *

         *     {@code ./src/(main|test)/(java|resources)
         *     }
         * 
* *

When the test is not in the same module as the code that * needs the generational support then the directories should be * code generation then the directories are * *

         *     {@code $root/module/src/(main|test)/(java|resources)
         *     }
         * 
*

* or * *

         *     {@code ./module/src/(main|test)/(java|resources)
         *     }
         * 
*

* where {@code $root} is where the root module is. In this case * the algorithm will also try {@code ..} as {@code $root}, * which works if the module structure is only two levels. * *

In other cases when the structure level is more than two * the test should specify the source directories calling the * static method {@link Source#maven(String)} specifying the * value for {@code $root} instead of simply calling {@link * Source#maven()}. The specified {@code $root} is usually * simply '{@code ../..}' in case of three levels of maven * modules. * *

This method calculates these directories and returns the * arrays. * * @param mainOrTest is either '{@code main}' or '{@code test}' * @param javaOrResources is either '{@code java}' or '{@code resources}' * @return the array of directory names where the generator has * to look for the sources */ private NamedSourceSet source(String name, String mainOrTest, String javaOrResources) { if (module == null) { return new NamedSourceSet(Set.set(name, true), new String[]{"./src/" + mainOrTest + "/" + javaOrResources}); } else { if (rootModuleDir == null) { return new NamedSourceSet(Set.set(name, true), new String[]{ "./" + module + "/src/" + mainOrTest + "/" + javaOrResources, "../" + module + "/src/" + mainOrTest + "/" + javaOrResources, "./src/" + mainOrTest + "/" + javaOrResources }); } else { return new NamedSourceSet(Set.set(name, true), new String[]{ rootModuleDir + "/" + module + "/src/" + mainOrTest + "/" + javaOrResources, "./" + module + "/src/" + mainOrTest + "/" + javaOrResources }); } } } /** * See also {@link #source(String, String, String)} * * @return the array of directories where the main java sources could be found. */ public NamedSourceSet mainSource() { return source(Geci.MAIN_SOURCE, "main", "java"); } /** * See also {@link #source(String, String, String)} * * @return the array of directories where the test java sources could be found. */ public NamedSourceSet testSource() { return source(Geci.TEST_SOURCE, "test", "java"); } /** * See also {@link #source(String, String, String)} * * @return the array of directories where the test resource files could be found. */ public NamedSourceSet testResources() { return source(Geci.TEST_RESOURCES, "test", "resources"); } /** * See also {@link #source(String, String, String)} * * @return the array of directories where the main resource files could be found. */ public NamedSourceSet mainResources() { return source(Geci.MAIN_RESOURCES, "main", "resources"); } /** * Specify the maven module where the sources needing the code generation are. * * @param module the name of the maven module * @return {@code this} */ public Maven module(String module) { this.module = module; return this; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy