001/*license*\
002   Codelet: Copyright (C) 2014, Jeff Epstein (aliteralmind __DASH__ github __AT__ yahoo __DOT__ com)
003
004   This software is dual-licensed under the:
005   - Lesser General Public License (LGPL) version 3.0 or, at your option, any later version;
006   - Apache Software License (ASL) version 2.0.
007
008   Either license may be applied at your discretion. More information may be found at
009   - http://en.wikipedia.org/wiki/Multi-licensing.
010
011   The text of both licenses is available in the root directory of this project, under the names "LICENSE_lgpl-3.0.txt" and "LICENSE_asl-2.0.txt". The latest copies may be downloaded at:
012   - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
013   - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
014\*license*/
015package  com.github.aliteralmind.codelet;
016   import  com.github.aliteralmind.templatefeather.FeatherTemplate;
017   import  com.github.aliteralmind.templatefeather.GapMap;
018   import  com.github.aliteralmind.templatefeather.IncorrectGapsException;
019   import  com.github.aliteralmind.templatefeather.Resettable;
020   import  com.github.aliteralmind.templatefeather.TemplateValidationUtil;
021   import  com.github.xbn.array.CrashIfArray;
022   import  com.github.xbn.array.NullContainer;
023   import  com.github.xbn.io.PlainTextFileUtil;
024   import  com.github.xbn.io.SimpleDebuggable;
025   import  com.github.xbn.io.TextAppenter;
026   import  com.github.xbn.lang.CrashIfObject;
027   import  com.github.xbn.lang.Null;
028   import  com.github.xbn.text.CrashIfString;
029   import  java.util.Arrays;
030   import  java.util.HashSet;
031   import  java.util.Map;
032   import  java.util.Objects;
033   import  java.util.Set;
034   import  java.util.TreeMap;
035   import  static com.github.aliteralmind.codelet.CodeletBaseConfig.*;
036/**
037   <p>What the fully-processed output (source-code, console output, or plain-file text) is put into--The rendered template is what actually replaces the codelet-taglet.</p>
038
039   <h2><a href="{@docRoot}/overview-summary.html#overview_description"><IMG SRC="{@docRoot}/resources/up_arrow.gif"/></a> &nbsp; Codelet: <u>Templates: Overview</u></h2>
040
041   <p>Codelet templates have one or two required &quot;body&quot; gaps, where the fully-processed text (source-code, console output, or plain-file text) is placed. The {@code {@codelet.and.out}} template has two body gaps, the rest have one. Each template type defines &quot;<b>optional-default</b>&quot; gaps, which you may place in your templates at your discretion. &quot;<b>Extra user-created</b>&quot; gaps are permitted if {@linkplain CodeletBaseConfig#USER_EXTRA_GAPS_CLASS_NAME explicitely configured}.</p>
042
043   <p><i>All gaps, in all Codelet templates (including user-created), are automatically {@linkplain CodeletGap#getFillText(CodeletInstance) filled}.</i></p>
044
045 * @since  0.1.0
046 * @author  Copyright (C) 2014, Jeff Epstein ({@code aliteralmind __DASH__ github __AT__ yahoo __DOT__ com}), dual-licensed under the LGPL (version 3.0 or later) or the ASL (version 2.0). See source code for details. <a href="http://codelet.aliteralmind.com">{@code http://codelet.aliteralmind.com}</a>, <a href="https://github.com/aliteralmind/codelet">{@code https://github.com/aliteralmind/codelet}</a>
047 **/
048public abstract class CodeletTemplateBase extends SimpleDebuggable  {
049   private final CodeletType            type            ;
050   private final FeatherTemplate        template        ;
051   private final String                 tmplPath        ;
052   private final Map<String,CodeletGap> allNonBodyGapMap;
053   /**
054      <p>Create the first instance only. To avoid circular dependencies, this class cannot have any references to {@link com.github.aliteralmind.codelet.CodeletBaseConfig}.</p>
055
056    * @param  type  May not be {@code null}. Get with {@link #getType() getType}{@code ()}.
057    * @param  template  May not be {@code null}, must be {@linkplain com.github.aliteralmind.templatefeather.FeatherTemplate#isResettable() resettable}, and must contain all body gaps, and no gaps that are not either optional-default or user-extra. This is duplicated (defensively copied). Get with {@link #getTemplate() getTemplate}{@code ()}.
058    * @param  tmpl_path  The full path to the template file. May not be {@code null} or empty. Get with {@link #getPath() getPath}{@code ()}
059    * @param  required_bodyGapNames  The one or two required body-gap names.
060    * @param  optional_defaultGaps  The optional-default gaps for the codelet type. May not be {@code null} or contain {@code null} elements, and all gap {@linkplain com.github.xbn.keyed.Named#getName() names} must be unique.
061    * @param  userExtra_gapGetter  Extra user-configured gaps. If {@code null}, there are no extra. Otherwise, the gaps its function (of type {@code type}) returns may not be {@code null} or contain {@code null} elements, and all gap names must be unique <i>and not equal to the body gaps or those in {@code optional_defaultGaps}</i>.
062    * @exception  IncorrectGapsException  If<ul>
063         <li>The length of {@code required_bodyGapNames} is invalid</li>
064         <li>The user-extra gaps contain a body or optional-default gap</li>
065         <li>The template is missing a body gap or contains any not in either the optional-defaults or user-extra</li>
066      </ul></li>
067    * @exception  IllegalArgumentException  If the {@code userExtra_gapGetter} function of type {@code type} returns {@code null}.
068    * @see  #CodeletTemplateBase(CodeletTemplateBase, FeatherTemplate, String)
069    * @see  #CodeletTemplateBase(CodeletTemplateBase, Appendable)
070    */
071   public CodeletTemplateBase(CodeletType type, FeatherTemplate template, String tmpl_path, String[] required_bodyGapNames, CodeletGap[] optional_defaultGaps, UserExtraGapGetter userExtra_gapGetter)  {
072      Objects.requireNonNull(type, "type");
073      CrashIfString.nullEmpty(tmpl_path, "tmpl_path", null);
074      CrashIfArray.lengthLessThan(required_bodyGapNames, "required_bodyGapNames", NullContainer.BAD, 1, null);
075      if(required_bodyGapNames.length > 2)  {
076         throw  new IncorrectGapsException("required_bodyGapNames.length (" + required_bodyGapNames.length + ") must be two or one. required_bodyGapNames=" + Arrays.toString(required_bodyGapNames));
077      }
078      TemplateValidationUtil.crashIfNotResettable(template, "template");
079      this.template = template.getObjectCopy();
080
081      this.type = type;
082      tmplPath = tmpl_path;
083
084      CodeletGap[] usrXtraGaps = null;
085      if(userExtra_gapGetter != null)  {
086         switch(type)  {
087            case SOURCE_CODE:
088               usrXtraGaps = userExtra_gapGetter.getForSourceCodelet();
089               break;
090            case CONSOLE_OUT:
091               usrXtraGaps = userExtra_gapGetter.getForCodeletDotOut();
092               break;
093            case SOURCE_AND_OUT:
094               usrXtraGaps = userExtra_gapGetter.getForCodeletAndOut();
095               break;
096            case FILE_TEXT:
097               usrXtraGaps = userExtra_gapGetter.getForFileTextlet();
098               break;
099            default:  throw  new IllegalStateException("Unknown type: " + type);
100         }
101         if(usrXtraGaps == null)  {
102            throw  new IllegalArgumentException("userExtra_gapGetter.getFor*() returned null. For no gaps, it must return an empty array.");
103         }
104      }  else  {
105         usrXtraGaps = new CodeletGap[]{};
106      }
107
108      TemplateValidationUtil.crashIfMissingRequiredGaps(template, "template", required_bodyGapNames);
109      for(String name : required_bodyGapNames)  {
110         for(CodeletGap gap : usrXtraGaps)  {
111            try  {
112               if(gap.getName().equals(name))  {
113                  throw  new IncorrectGapsException("Body gap named \"" + name + "\" found in user-extra gaps.");
114               }
115            }  catch(RuntimeException rx)  {
116               throw  CrashIfObject.nullOrReturnCause(gap, "[One of the user-extra gaps]", null, rx);
117            }
118         }
119      }
120
121      allNonBodyGapMap = CodeletTemplateBase.newGapMapFromArray(optional_defaultGaps);
122      CodeletTemplateBase.addCustomGaps(allNonBodyGapMap, usrXtraGaps);
123
124      //user-extras have no duplicates.
125
126      //Crash if the template has any unexpected (not optional-default
127      //or user-extra) gaps
128      Set<String> unexpectedNameSet = new HashSet<String>(template.getGapMap().size());
129      unexpectedNameSet.addAll(template.getGapMap().newNameSet());
130      for(String name : required_bodyGapNames)  {
131         unexpectedNameSet.remove(name);
132      }
133      unexpectedNameSet.removeAll(allNonBodyGapMap.keySet());
134
135      if(unexpectedNameSet.size() > 0)  {
136         throw  new IncorrectGapsException("Unexpected gaps found in template: " + Arrays.toString(unexpectedNameSet.toArray()) + ". Body gap names: " + Arrays.toString(required_bodyGapNames) + ", Optional-default *and* user-extra: " + Arrays.toString(allNonBodyGapMap.keySet().toArray()));
137      }
138   }
139   /**
140      <p>Create the second or subsequent instance.</p>
141
142      <h4>Automated {@linkplain CodeletBootstrap#CODELET_RQD_NAMED_DBGRS_CONFIG_FILE named debuggers}</h4>
143
144      <p>{@code zzCodeletTemplateBase.templateparseandfill}: <code>{@link com.github.aliteralmind.templatefeather.FeatherTemplate}.</code>{@linkplain com.github.aliteralmind.templatefeather.FeatherTemplate#FeatherTemplate(FeatherTemplate, Appendable) constructor}</p>
145
146    * @param  to_copy  May not be {@code null}.
147    * @param  template  May not be {@code null}, must have all body gaps, and may not have any gaps that are not either optional-default or user-extra. This is duplicated (defensively copied). Get with {@link #getTemplate() getTemplate}{@code ()}.
148    * @see  #CodeletTemplateBase(CodeletType, FeatherTemplate, String, String[], CodeletGap[], UserExtraGapGetter)
149    * @see  #CodeletTemplateBase(CodeletTemplateBase, Appendable)
150    */
151   public CodeletTemplateBase(CodeletTemplateBase to_copy, FeatherTemplate template, String tmpl_path)  {
152
153      try  {
154         type = to_copy.getType();
155      }  catch(RuntimeException rx)  {
156         throw  CrashIfObject.nullOrReturnCause(to_copy, "to_copy", null, rx);
157      }
158      allNonBodyGapMap = new TreeMap<String,CodeletGap>();
159      allNonBodyGapMap.putAll(to_copy.allNonBodyGapMap);
160
161      Appendable dbgTmpl = getDebugApblIfOn(null,
162         "zzCodeletTemplateBase.newTemplateFromPath.templateparseandfill");
163      try  {
164         this.template = new FeatherTemplate(template, dbgTmpl);
165      }  catch(RuntimeException rx)  {
166         throw  CrashIfObject.nullOrReturnCause(template, "template", null, rx);
167      }
168      CrashIfString.nullEmpty(tmpl_path, "tmpl_path", null);
169      tmplPath = tmpl_path;
170   }
171   /**
172      <p>Create a new instance as a duplicate of another.</p>
173
174      <p>This calls<ol>
175         <li>{@link #CodeletTemplateBase(CodeletTemplateBase, FeatherTemplate, String) this}(to_copy, to_copy.getTemplate(), to_copy.getPath())</li>
176         <li>{@link com.github.xbn.io.SimpleDebuggable#onIfNonNull(Appendable) onIfNonNull}{@code (debugDest_ifNonNull)}</li>
177      </ol></p>
178    */
179   public CodeletTemplateBase(CodeletTemplateBase to_copy, Appendable debugDest_ifNonNull)  {
180      this(to_copy,
181         ((to_copy == null) ? null : to_copy.getTemplate()),
182         ((to_copy == null) ? null : to_copy.getPath()));
183      onIfNonNull(debugDest_ifNonNull);
184   }
185
186   /**
187      <p>The type of this template.</p>
188
189    * @see  #CodeletTemplateBase(CodeletType, FeatherTemplate, String, String[], CodeletGap[], UserExtraGapGetter)
190    */
191   public CodeletType getType()  {
192      return  type;
193   }
194   /**
195      <p>Fill a body gap.</p>
196
197    * <p>Equal to</p>
198
199      <p><code>{@link #getTemplate() getTemplate}().{@link com.github.aliteralmind.templatefeather.FeatherTemplate#fill(String, Object) fill}(body_gapName, fill_with)</code></p>
200
201    * @param  body_gapName  The body gap name. Must not have already been filled.
202    * @param  body_text  May not be {@code null} or empty.
203    */
204   public void fillBodyGap(String body_gapName, String body_text)  {
205      CrashIfString.empty(Null.OK, body_text, body_gapName, null);
206      getTemplate().fill(body_gapName, body_text);
207   }
208   /**
209      <p>ReplacedInEachInput all default-optional and user-extra gaps with their values, resets the template, and returns its fully-rendered text--This is what actually replaces the codelet-taglet.</p>
210
211    * @param  instance  May not be {@code null}.
212    * @exception  IllegalArgumentException  If any gaps are already filled, or if the com.github.aliteralmind.codelet.CodeletGap#getFillText(CodeletInstance) fill text is {@code null} or empty.
213    * @see  #fillBodyGap(String, String)
214    */
215   public String getRendered(CodeletInstance instance)  {
216      Set<String> nameSet = allNonBodyGapMap.keySet();
217      for(String name : nameSet)  {
218         if(!getTemplate().getGapMap().contains(name))  {
219            continue;
220         }
221         String fillText = null;
222         try  {
223            fillText = allNonBodyGapMap.get(name).getFillText(instance);
224            if(fillText.length() == 0)  {
225               throw  new IllegalArgumentException("Fill text for CodeletGap named \"" + name + "\" has no characters.");
226            }
227         }  catch(RuntimeException rx)  {
228            CrashIfObject.nnull(instance, "instance", null);
229            throw  CrashIfObject.nullOrReturnCause(fillText, "[Fill text for CodeletGap named \"" + name + "\"]", null, rx);
230         }
231         getTemplate().fill(name, fillText);
232      }
233      return  getTemplate().getFilledAndReset();
234   }
235   /**
236      <p>The number of gaps actually in the template.</p>
237
238    * @return  <code>{@link #getTemplate() getTemplate}().{@link com.github.aliteralmind.templatefeather.FeatherTemplate#getGapMap() getGapMap}().size()</code>
239    */
240   public int getGapCount()  {
241      return  getTemplate().getGapMap().size();
242   }
243   /**
244      <p>The template.</p>
245
246    * @see  #getGapCount()
247    * @see  #CodeletTemplateBase(CodeletType, FeatherTemplate, String, String[], CodeletGap[], UserExtraGapGetter) CodeletTemplateBase(ct,ft,s,s[],cg[],uxgg)
248    */
249   protected FeatherTemplate getTemplate()  {
250      return  template;
251   }
252   public String getPath()  {
253      return  tmplPath;
254   }
255   public abstract CodeletTemplateBase getObjectCopy(Appendable debugDest_ifNonNull);
256   /**
257      <p>Create a new map of all gap objects.</p>
258
259    * @param  gap_array  May not be {@code null} or contain {@code null} elements, and all gap {@linkplain com.github.xbn.keyed.Named#getName() names} must be unique.
260    * @exception  IllegalArgumentException  If a gap name is used more than once.
261    * @see  #addCustomGaps(Map, CodeletGap[])
262    */
263   public static final Map<String,CodeletGap> newGapMapFromArray(CodeletGap[] gap_array)  {
264      return  newGapMapFromArray(gap_array, true);
265   }
266      private static final Map<String,CodeletGap> newGapMapFromArray(CodeletGap[] gap_array, boolean do_crashIfDupName)  {
267         Map<String,CodeletGap> optionalDefault_gapMap = new TreeMap<String,CodeletGap>();
268         try  {
269            for(int i = 0; i < gap_array.length; i++)  {
270               CodeletGap gap = gap_array[i];
271               try  {
272                  if(do_crashIfDupName  &&  optionalDefault_gapMap.containsKey(gap.getName()))  {
273                     throw  new IllegalArgumentException("Duplicate gap name: gap_array[" + i + "].getName() (\"" + gap.getName() + "\"). All names: " + Arrays.toString(gap_array));
274                  }
275               }  catch(RuntimeException rx)  {
276                  throw  CrashIfObject.nullOrReturnCause(gap, "gap_array[" + i + "]", null, rx);
277               }
278               optionalDefault_gapMap.put(gap.getName(), gap);
279            }
280         }  catch(RuntimeException rx)  {
281            throw  CrashIfObject.nullOrReturnCause(gap_array, "gap_array", null, rx);
282         }
283         return  optionalDefault_gapMap;
284      }
285   /**
286      <p>Add custom gaps to the template.</p>
287
288    * @param  optionalDefault_gapMap  May not be {@code null}, and must contain only the ...default gaps....
289    * @param  gap_array  May not be {@code null} or contain {@code null} elements, and all gap {@linkplain com.github.xbn.keyed.Named#getName() names} must be unique <i>and not contain any in {@code optionalDefault_gapMap}.</i>
290    * @exception  IllegalArgumentException  If {@code gap_array} contains a duplicate or default gap name (or they've already been added to the template!).
291    */
292   public static final void addCustomGaps(Map<String,CodeletGap> optionalDefault_gapMap, CodeletGap[] gap_array)  {
293      Map<String,CodeletGap> paramGapMap = newGapMapFromArray(gap_array);
294      Set<String> paramNameSet = paramGapMap.keySet();
295      for(String name : paramNameSet)  {
296         if(optionalDefault_gapMap.containsKey(name))  {
297            throw  new IllegalArgumentException("gap_array contains a default gap name: \"" + name + "\". Default gap names: " + Arrays.toString(optionalDefault_gapMap.keySet().toArray()) + ". Names in gap_array: " + Arrays.toString(paramNameSet.toArray()));
298         }
299      }
300      optionalDefault_gapMap.putAll(optionalDefault_gapMap);
301   }
302   /**
303      <p>Read in and parse a template given its path.</p>
304
305      <h4>Automated {@linkplain CodeletBootstrap#CODELET_RQD_NAMED_DBGRS_CONFIG_FILE named debuggers}</h4>
306
307      <p>{@code zzCodeletTemplateBase.newTemplateFromPath.}<ul>
308         <li>{@code loading}: &quot;Loading template from <i>[path]</i>...&quot;</li>
309         <li>{@code templateparseandfill}: <code>{@link com.github.aliteralmind.templatefeather.FeatherTemplate}.</code>{@linkplain com.github.aliteralmind.templatefeather.FeatherTemplate#FeatherTemplate(String, GapCharConfig, Resettable, Appendable) constructor}</li>
310      </ul></p>
311
312    * @see  com.github.aliteralmind.codelet.type.SourceCodeTemplate#newFromPathAndUserExtraGaps(String, String, UserExtraGapGetter) SourceCodeTemplate#newFromPathAndUserExtraGaps
313    * @see  com.github.aliteralmind.codelet.type.SourceAndOutTemplate#newFromPathAndUserExtraGaps(String, String, UserExtraGapGetter) SourceAndOutTemplate#newFromPathAndUserExtraGaps
314    * @see  com.github.aliteralmind.codelet.type.ConsoleOutTemplate#newFromPathAndUserExtraGaps(String, String, UserExtraGapGetter) ConsoleOutTemplate#newFromPathAndUserExtraGaps
315    * @see  com.github.aliteralmind.codelet.type.FileTextTemplate#newFromPathAndUserExtraGaps(String, String, UserExtraGapGetter) FileTextTemplate#newFromPathAndUserExtraGaps
316    */
317   public static final FeatherTemplate newTemplateFromPath(String path, String path_varName, String... required_gaps)  {
318      String prefix = "zzCodeletTemplateBase.newTemplateFromPath.";
319      TextAppenter tbgLoading = getDebugAptrIfOn(null, prefix + "loading");
320      Appendable dbgTmpl = getDebugApblIfOn(null,
321         prefix + "templateparseandfill");
322
323      if(tbgLoading != null)  {
324         tbgLoading.appentln("      Loading " + path_varName + ": \"" + path+ "\"");
325      }
326
327      FeatherTemplate tmpl = new FeatherTemplate(
328         PlainTextFileUtil.getText(path, path_varName),
329         getGapCharConfig(), Resettable.YES, dbgTmpl);
330      TemplateValidationUtil.crashIfMissingRequiredGaps(tmpl, path_varName, required_gaps);
331      return  tmpl;
332   }
333   public void setDebug(Appendable destination, boolean is_on)  {
334      if(getTemplate() != null)  {
335         //This is *not* a call from the constructor (before the template exists).
336         getTemplate().setDebug(destination, is_on);
337      }
338      super.setDebug(destination, is_on);
339   }
340   public void setDebugOn(boolean is_on)  {
341      if(getTemplate() != null)  {
342         //This is *not* a call from the constructor (before the template exists).
343         getTemplate().setDebugOn(is_on);
344      }
345      super.setDebugOn(is_on);
346   }
347}