17
17
18
18
import java .lang .reflect .Method ;
19
19
import java .util .ArrayDeque ;
20
+ import java .util .ArrayList ;
21
+ import java .util .Arrays ;
20
22
import java .util .Deque ;
21
23
import java .util .HashMap ;
22
24
import java .util .Iterator ;
25
+ import java .util .List ;
23
26
import java .util .Map ;
24
27
import java .util .Objects ;
25
28
import java .util .Optional ;
574
577
*/
575
578
public abstract class AbstractCodeWriter <T extends AbstractCodeWriter <T >> {
576
579
580
+ // Valid formatter characters that can be registered. Must be sorted for binary search to work.
581
+ static final char [] VALID_FORMATTER_CHARS = {
582
+ '!' , '%' , '&' , '*' , '+' , ',' , '-' , '.' , ';' , '=' , '@' ,
583
+ 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' ,
584
+ 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' , '_' , '`' };
585
+
577
586
private static final Pattern LINES = Pattern .compile ("\\ r?\\ n" );
578
587
private static final Map <Character , BiFunction <Object , String , String >> DEFAULT_FORMATTERS = MapUtils .of (
579
588
'L' , (s , i ) -> formatLiteral (s ),
@@ -603,22 +612,19 @@ public AbstractCodeWriter() {
603
612
/**
604
613
* Copies settings from the given AbstractCodeWriter into this AbstractCodeWriter.
605
614
*
606
- * <p>The settings of the {@code other} AbstractCodeWriter will overwrite
607
- * both global and state-based settings of this AbstractCodeWriter. Formatters of
608
- * the {@code other} AbstractCodeWriter will be merged with the formatters of this
609
- * AbstractCodeWriter, and in the case of conflicts, the formatters of the
610
- * {@code other} will take precedence.
615
+ * <p>The settings of the {@code other} AbstractCodeWriter will overwrite both global and state-based settings
616
+ * of this AbstractCodeWriter.
611
617
*
612
- * <p>Stateful settings of the {@code other} AbstractCodeWriter are copied into
613
- * the <em>current</em> state of this AbstractCodeWriter. Only the settings of
614
- * the top-most state is copied. Other states, and the contents of the
615
- * top-most state are not copied.
618
+ * <p>Stateful settings of the {@code other} AbstractCodeWriter like formatters, interceptors, and context are
619
+ * flattened and then copied into the <em>current</em> state of this AbstractCodeWriter. Any conflicts between
620
+ * formatters, interceptors, or context of the current writer are overwritten by the other writer. The stack of
621
+ * states and the contents written to {@code other} are not copied.
616
622
*
617
623
* <pre>{@code
618
- * SimpleCodeWritera = new SimpleCodeWriter();
624
+ * SimpleCodeWriter a = new SimpleCodeWriter();
619
625
* a.setExpressionStart('#');
620
626
*
621
- * SimpleCodeWriterb = new SimpleCodeWriter();
627
+ * SimpleCodeWriter b = new SimpleCodeWriter();
622
628
* b.copySettingsFrom(a);
623
629
*
624
630
* assert(b.getExpressionStart() == '#');
@@ -634,6 +640,16 @@ public void copySettingsFrom(AbstractCodeWriter<T> other) {
634
640
635
641
// Copy the current state settings of other into the current state.
636
642
currentState .copyStateFrom (other .currentState );
643
+
644
+ // Flatten containers into the current state. This is done in reverse order to ensure that more recent
645
+ // state changes supersede earlier changes.
646
+ Iterator <State > reverseOtherStates = other .states .descendingIterator ();
647
+ while (reverseOtherStates .hasNext ()) {
648
+ State otherState = reverseOtherStates .next ();
649
+ currentState .interceptors .addAll (otherState .interceptors );
650
+ currentState .formatters .putAll (otherState .formatters );
651
+ currentState .context .putAll (otherState .context );
652
+ }
637
653
}
638
654
639
655
/**
@@ -686,7 +702,7 @@ public static String formatLiteral(Object value) {
686
702
*/
687
703
@ SuppressWarnings ("unchecked" )
688
704
public T putFormatter (char identifier , BiFunction <Object , String , String > formatFunction ) {
689
- this .currentState .formatters . get (). putFormatter (identifier , formatFunction );
705
+ this .currentState .putFormatter (identifier , formatFunction );
690
706
return (T ) this ;
691
707
}
692
708
@@ -962,9 +978,15 @@ public T popState() {
962
978
963
979
// Don't attempt to intercept anonymous sections.
964
980
if (!(sectionValue instanceof AnonymousCodeSection )) {
965
- for (CodeInterceptor <CodeSection , T > interceptor : popped .interceptors .peek ().get (sectionValue )) {
966
- result = interceptSection (popped , interceptor , result );
981
+ // Ensure the remaining parent interceptors are applied in the order they were inserted.
982
+ // This is the reverse order used when normally iterating over the states deque.
983
+ Iterator <State > insertionOrderedStates = states .descendingIterator ();
984
+ while (insertionOrderedStates .hasNext ()) {
985
+ State state = insertionOrderedStates .next ();
986
+ result = applyPoppedInterceptors (popped , state , sectionValue , result );
967
987
}
988
+ // Now ensure the popped state's interceptors are applied.
989
+ result = applyPoppedInterceptors (popped , popped , sectionValue , result );
968
990
}
969
991
970
992
if (popped .isInline ) {
@@ -986,6 +1008,13 @@ public T popState() {
986
1008
return (T ) this ;
987
1009
}
988
1010
1011
+ private String applyPoppedInterceptors (State popped , State state , CodeSection sectionValue , String result ) {
1012
+ for (CodeInterceptor <CodeSection , T > interceptor : state .getInterceptors (sectionValue )) {
1013
+ result = interceptSection (popped , interceptor , result );
1014
+ }
1015
+ return result ;
1016
+ }
1017
+
989
1018
// This method exists because inlining in popSection is impossible due to needing to mutate a result variable.
990
1019
@ SuppressWarnings ("unchecked" )
991
1020
private String interceptSection (State popped , CodeInterceptor <CodeSection , T > interceptor , String previous ) {
@@ -1070,7 +1099,7 @@ private String interceptSection(State popped, CodeInterceptor<CodeSection, T> in
1070
1099
*/
1071
1100
@ SuppressWarnings ("unchecked" )
1072
1101
public T onSection (String sectionName , Consumer <Object > interceptor ) {
1073
- currentState .interceptors . get (). putInterceptor (CodeInterceptor .forName (sectionName , (w , p ) -> {
1102
+ currentState .putInterceptor (CodeInterceptor .forName (sectionName , (w , p ) -> {
1074
1103
String trimmedContent = removeTrailingNewline (p );
1075
1104
interceptor .accept (trimmedContent );
1076
1105
}));
@@ -1098,7 +1127,7 @@ public T onSection(String sectionName, Consumer<Object> interceptor) {
1098
1127
*/
1099
1128
@ SuppressWarnings ("unchecked" )
1100
1129
public <S extends CodeSection > T onSection (CodeInterceptor <S , T > interceptor ) {
1101
- currentState .interceptors . get (). putInterceptor (interceptor );
1130
+ currentState .putInterceptor (interceptor );
1102
1131
return (T ) this ;
1103
1132
}
1104
1133
@@ -1857,7 +1886,7 @@ public T unwrite(Object content, Object... args) {
1857
1886
*/
1858
1887
@ SuppressWarnings ("unchecked" )
1859
1888
public T putContext (String key , Object value ) {
1860
- currentState .context .get (). put (key , value );
1889
+ currentState .context .put (key , value );
1861
1890
return (T ) this ;
1862
1891
}
1863
1892
@@ -1879,40 +1908,45 @@ public T putContext(Map<String, Object> mappings) {
1879
1908
/**
1880
1909
* Removes a named key-value pair from the context of the current state.
1881
1910
*
1911
+ * <p>This method has no effect if the parent state defines the context key value pair.
1912
+ *
1882
1913
* @param key Key to add to remove from the current context.
1883
1914
* @return Returns self.
1884
1915
*/
1885
1916
@ SuppressWarnings ("unchecked" )
1886
1917
public T removeContext (String key ) {
1887
- if (currentState .context .peek ().containsKey (key )) {
1888
- currentState .context .get ().remove (key );
1918
+ if (currentState .context .containsKey (key )) {
1919
+ currentState .context .remove (key );
1920
+ } else {
1921
+ // Parent states might have a value for this context key, so explicitly set it to null in this context.
1922
+ currentState .context .put (key , null );
1889
1923
}
1890
1924
return (T ) this ;
1891
1925
}
1892
1926
1893
1927
/**
1894
- * Gets a named contextual key-value pair from the current state.
1928
+ * Gets a named contextual key-value pair from the current state or any parent states .
1895
1929
*
1896
1930
* @param key Key to retrieve.
1897
1931
* @return Returns the associated value or null if not present.
1898
1932
*/
1899
1933
public Object getContext (String key ) {
1900
- CodeSection section = currentState . sectionValue ;
1901
- Map < String , Object > currentContext = currentState .context .peek ();
1902
- if ( currentContext . containsKey (key )) {
1903
- return currentContext . get ( key );
1904
- } else if ( section != null ) {
1905
- Method method = findContextMethod ( section , key );
1906
- if ( method != null ) {
1907
- try {
1908
- return method . invoke ( section );
1909
- } catch ( ReflectiveOperationException e ) {
1910
- String message = String . format (
1911
- "Unable to get context '%s' from a matching method of the current CodeSection: %s %s" ,
1912
- key ,
1913
- e . getCause () != null ? e . getCause (). getMessage () : e . getMessage (),
1914
- getDebugInfo () );
1915
- throw new RuntimeException ( message , e );
1934
+ for ( State state : states ) {
1935
+ if ( state .context .containsKey ( key )) {
1936
+ return state . context . get (key );
1937
+ } else if ( state . sectionValue != null ) {
1938
+ Method method = findContextMethod ( state . sectionValue , key );
1939
+ if ( method != null ) {
1940
+ try {
1941
+ return method . invoke ( state . sectionValue );
1942
+ } catch ( ReflectiveOperationException e ) {
1943
+ String message = String . format (
1944
+ "Unable to get context '%s' from a matching method of the current CodeSection: %s %s" ,
1945
+ key ,
1946
+ e . getCause () != null ? e . getCause (). getMessage () : e . getMessage () ,
1947
+ getDebugInfo ());
1948
+ throw new RuntimeException ( message , e );
1949
+ }
1916
1950
}
1917
1951
}
1918
1952
}
@@ -1984,7 +2018,7 @@ String expandSection(CodeSection section, String previousContent, Consumer<Strin
1984
2018
// Used only by CodeFormatter to apply formatters.
1985
2019
@ SuppressWarnings ("unchecked" )
1986
2020
String applyFormatter (char identifier , Object value ) {
1987
- BiFunction <Object , String , String > f = currentState . formatters . peek (). getFormatter (identifier );
2021
+ BiFunction <Object , String , String > f = resolveFormatter (identifier );
1988
2022
if (f != null ) {
1989
2023
return f .apply (value , getIndentText ());
1990
2024
} else if (identifier == 'C' ) {
@@ -2009,14 +2043,24 @@ String applyFormatter(char identifier, Object value) {
2009
2043
throw new ClassCastException (String .format (
2010
2044
"Expected value for 'C' formatter to be an instance of %s or %s, but found %s %s" ,
2011
2045
Runnable .class .getName (), Consumer .class .getName (),
2012
- value .getClass ().getName (), getDebugInfo ()));
2046
+ value == null ? "null" : value .getClass ().getName (), getDebugInfo ()));
2013
2047
}
2014
2048
} else {
2015
2049
// Return null if no formatter was found.
2016
2050
return null ;
2017
2051
}
2018
2052
}
2019
2053
2054
+ BiFunction <Object , String , String > resolveFormatter (char identifier ) {
2055
+ for (State state : states ) {
2056
+ BiFunction <Object , String , String > result = state .getFormatter (identifier );
2057
+ if (result != null ) {
2058
+ return result ;
2059
+ }
2060
+ }
2061
+ return null ;
2062
+ }
2063
+
2020
2064
private final class State {
2021
2065
private final boolean isRoot ;
2022
2066
private String indentText = " " ;
@@ -2030,9 +2074,9 @@ private final class State {
2030
2074
private boolean needsIndentation ;
2031
2075
2032
2076
private CodeSection sectionValue ;
2033
- private CopyOnWriteRef < Map <String , Object >> context ;
2034
- private CopyOnWriteRef < CodeWriterFormatterContainer > formatters ;
2035
- private CopyOnWriteRef < CodeInterceptorContainer < T >> interceptors ;
2077
+ private final Map <String , Object > context = new HashMap <>() ;
2078
+ private final Map < Character , BiFunction < Object , String , String >> formatters = new HashMap <>() ;
2079
+ private final List < CodeInterceptor < CodeSection , T >> interceptors = new ArrayList <>() ;
2036
2080
2037
2081
private StringBuilder builder ;
2038
2082
@@ -2046,11 +2090,7 @@ private final class State {
2046
2090
State () {
2047
2091
builder = new StringBuilder ();
2048
2092
isRoot = true ;
2049
- CodeWriterFormatterContainer formatterContainer = new CodeWriterFormatterContainer ();
2050
- DEFAULT_FORMATTERS .forEach (formatterContainer ::putFormatter );
2051
- this .formatters = CopyOnWriteRef .fromOwned (formatterContainer );
2052
- this .context = CopyOnWriteRef .fromOwned (new HashMap <>());
2053
- this .interceptors = CopyOnWriteRef .fromOwned (new CodeInterceptorContainer <>());
2093
+ DEFAULT_FORMATTERS .forEach (this ::putFormatter );
2054
2094
}
2055
2095
2056
2096
@ SuppressWarnings ("CopyConstructorMissesField" )
@@ -2060,20 +2100,18 @@ private final class State {
2060
2100
this .builder = copy .builder ;
2061
2101
}
2062
2102
2103
+ // This does not copy context, interceptors, or formatters.
2104
+ // State inheritance relies on stacks of States in an AbstractCodeWriter.
2063
2105
private void copyStateFrom (State copy ) {
2064
2106
this .newline = copy .newline ;
2065
2107
this .expressionStart = copy .expressionStart ;
2066
- this .context = copy .context ;
2067
2108
this .indentText = copy .indentText ;
2068
2109
this .leadingIndentString = copy .leadingIndentString ;
2069
2110
this .indentation = copy .indentation ;
2070
2111
this .newlinePrefix = copy .newlinePrefix ;
2071
2112
this .trimTrailingSpaces = copy .trimTrailingSpaces ;
2072
2113
this .disableNewline = copy .disableNewline ;
2073
2114
this .needsIndentation = copy .needsIndentation ;
2074
- this .context = CopyOnWriteRef .fromBorrowed (copy .context .peek (), HashMap ::new );
2075
- this .formatters = CopyOnWriteRef .fromBorrowed (copy .formatters .peek (), CodeWriterFormatterContainer ::new );
2076
- this .interceptors = CopyOnWriteRef .fromBorrowed (copy .interceptors .peek (), CodeInterceptorContainer ::new );
2077
2115
}
2078
2116
2079
2117
@ Override
@@ -2200,6 +2238,48 @@ private String getSectionName() {
2200
2238
return sectionValue .sectionName ();
2201
2239
}
2202
2240
}
2241
+
2242
+ void putFormatter (Character identifier , BiFunction <Object , String , String > formatFunction ) {
2243
+ if (Arrays .binarySearch (VALID_FORMATTER_CHARS , identifier ) < 0 ) {
2244
+ throw new IllegalArgumentException ("Invalid formatter identifier: " + identifier );
2245
+ }
2246
+ formatters .put (identifier , formatFunction );
2247
+ }
2248
+
2249
+ BiFunction <Object , String , String > getFormatter (char identifier ) {
2250
+ return formatters .get (identifier );
2251
+ }
2252
+
2253
+ @ SuppressWarnings ("unchecked" )
2254
+ void putInterceptor (CodeInterceptor <? extends CodeSection , T > interceptor ) {
2255
+ interceptors .add ((CodeInterceptor <CodeSection , T >) interceptor );
2256
+ }
2257
+
2258
+ /**
2259
+ * Gets a list of interceptors that match the given type and for which the
2260
+ * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
2261
+ * when given {@code forSection}.
2262
+ *
2263
+ * @param forSection The section that is being intercepted.
2264
+ * @param <S> The type of section being intercepted.
2265
+ * @return Returns the list of matching interceptors.
2266
+ */
2267
+ <S extends CodeSection > List <CodeInterceptor <CodeSection , T >> getInterceptors (S forSection ) {
2268
+ // Add in parent interceptors.
2269
+ List <CodeInterceptor <CodeSection , T >> result = new ArrayList <>();
2270
+ // Merge in local interceptors.
2271
+ for (CodeInterceptor <CodeSection , T > interceptor : interceptors ) {
2272
+ // Add the interceptor only if it's the right type.
2273
+ if (interceptor .sectionType ().isInstance (forSection )) {
2274
+ // Only add if the filter passes.
2275
+ if (interceptor .isIntercepted (forSection )) {
2276
+ result .add (interceptor );
2277
+ }
2278
+ }
2279
+ }
2280
+
2281
+ return result ;
2282
+ }
2203
2283
}
2204
2284
2205
2285
String removeTrailingNewline (String value ) {
0 commit comments