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> Codelet: <u>Templates: Overview</u></h2> 040 041 <p>Codelet templates have one or two required "body" 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 "<b>optional-default</b>" gaps, which you may place in your templates at your discretion. "<b>Extra user-created</b>" 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}: "Loading template from <i>[path]</i>..."</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}