public class BeansWrapper extends Object implements RichObjectWrapper, WriteProtectable
ObjectWrapper
that is able to expose the Java API of arbitrary Java objects. This is also the superclass of
DefaultObjectWrapper
. Note that instances of this class generally should be created with a
BeansWrapperBuilder
, not with its public constructors.
As of 2.3.22, using BeansWrapper
unextended is not recommended. Instead, DefaultObjectWrapper
with
its incompatibleImprovements
property set to 2.3.22 (or higher) is the recommended ObjectWrapper
.
This class is only thread-safe after you have finished calling its setter methods, and then safely published it (see
JSR 133 and related literature). When used as part of Configuration
, of course it's enough if that was safely
published and then left unmodified. Using BeansWrapperBuilder
also guarantees thread safety.
Modifier and Type | Class and Description |
---|---|
static class |
BeansWrapper.MethodAppearanceDecision
Experimental class; subject to change!
Used for
MethodAppearanceFineTuner.process(freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput, freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision)
to store the results; see there. |
static class |
BeansWrapper.MethodAppearanceDecisionInput
Experimental class; subject to change!
Used for
MethodAppearanceFineTuner.process(freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput, freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision)
as input parameter; see there. |
Modifier and Type | Field and Description |
---|---|
static int |
EXPOSE_ALL
At this level of exposure, all methods and properties of the
wrapped objects are exposed to the template.
|
static int |
EXPOSE_NOTHING
At this level of exposure, no bean properties and methods are exposed.
|
static int |
EXPOSE_PROPERTIES_ONLY
At this level of exposure, only property getters are exposed.
|
static int |
EXPOSE_SAFE
At this level of exposure, all methods and properties of the wrapped
objects are exposed to the template except methods that are deemed
not safe.
|
CANT_UNWRAP_TO_TARGET_CLASS
BEANS_WRAPPER, DEFAULT_WRAPPER, SIMPLE_WRAPPER
Modifier | Constructor and Description |
---|---|
|
BeansWrapper()
Deprecated.
Use
BeansWrapperBuilder or, in rare cases, BeansWrapper(Version) instead. |
protected |
BeansWrapper(BeansWrapperConfiguration bwConf,
boolean writeProtected)
Same as
BeansWrapper(BeansWrapperConfiguration, boolean, boolean) with true
finalizeConstruction argument. |
protected |
BeansWrapper(BeansWrapperConfiguration bwConf,
boolean writeProtected,
boolean finalizeConstruction)
Initializes the instance based on the the
BeansWrapperConfiguration specified. |
|
BeansWrapper(Version incompatibleImprovements)
Use
BeansWrapperBuilder instead of the public constructors if possible. |
Modifier and Type | Method and Description |
---|---|
protected void |
checkModifiable()
If this object is already read-only according to
WriteProtectable , throws IllegalStateException ,
otherwise does nothing. |
void |
clearClassIntrospecitonCache()
Removes all class introspection data from the cache.
|
static Object |
coerceBigDecimal(BigDecimal bd,
Class formalType) |
static void |
coerceBigDecimals(AccessibleObject callable,
Object[] args)
Converts any
BigDecimal s in the passed array to the type of
the corresponding formal argument of the method. |
static void |
coerceBigDecimals(Class[] formalTypes,
Object[] args)
Converts any
BigDecimal s in the passed array to the type of
the corresponding formal argument of the method. |
protected void |
finalizeConstruction(boolean writeProtected)
Meant to be called after
BeansWrapper(BeansWrapperConfiguration, boolean, boolean) when
its last argument was false ; makes the instance read-only if necessary, then registers the model
factories in the class introspector. |
protected void |
finetuneMethodAppearance(Class clazz,
Method m,
BeansWrapper.MethodAppearanceDecision decision)
Deprecated.
Use
setMethodAppearanceFineTuner(MethodAppearanceFineTuner) ;
no need to extend this class anymore.
Soon this method will be final, so trying to override it will break your app.
Note that if the methodAppearanceFineTuner property is set to non-null , this method is not
called anymore. |
int |
getDefaultDateType()
Returns the default date type.
|
static BeansWrapper |
getDefaultInstance()
Deprecated.
Use
BeansWrapperBuilder instead. The instance returned here is not read-only, so it's
dangerous to use. |
TemplateHashModel |
getEnumModels()
Returns a hash model that represents the so-called class enum models.
|
int |
getExposureLevel() |
Version |
getIncompatibleImprovements()
Returns the version given with
BeansWrapper(Version) , normalized to the lowest version where a change
has occurred. |
protected TemplateModel |
getInstance(Object object,
ModelFactory factory)
Deprecated.
override
getModelFactory(Class) instead. Using this
method will now bypass wrapper caching (if it's enabled) and always
result in creation of a new wrapper. This method will be removed in 2.4 |
MethodAppearanceFineTuner |
getMethodAppearanceFineTuner() |
protected ModelFactory |
getModelFactory(Class clazz) |
ObjectWrapper |
getOuterIdentity()
By default returns this.
|
TemplateHashModel |
getStaticModels()
Returns a hash model that represents the so-called class static models.
|
boolean |
getUseCache() |
boolean |
isClassIntrospectionCacheRestricted()
Tells if this instance acts like if its class introspection cache is sharable with other
BeansWrapper -s. |
boolean |
isExposeFields()
Returns whether exposure of public instance fields of classes is
enabled.
|
boolean |
isSimpleMapWrapper()
Tells whether Maps are exposed as simple maps, without access to their
method.
|
boolean |
isStrict() |
boolean |
isWriteProtected() |
Object |
newInstance(Class clazz,
List arguments)
Creates a new instance of the specified class using the method call logic of this object wrapper for calling the
constructor.
|
protected static Version |
normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements)
Returns the lowest version number that is equivalent with the parameter version.
|
void |
removeFromClassIntrospectionCache(Class clazz)
Removes the introspection data for a class from the cache.
|
void |
setDefaultDateType(int defaultDateType)
Sets the default date type to use for date models that result from
a plain java.util.Date instead of java.sql.Date or
java.sql.Time or java.sql.Timestamp.
|
void |
setExposeFields(boolean exposeFields)
Controls whether public instance fields of classes are exposed to
templates.
|
void |
setExposureLevel(int exposureLevel)
Sets the method exposure level.
|
void |
setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner)
Used to tweak certain aspects of how methods appear in the data-model;
see
MethodAppearanceFineTuner for more. |
void |
setMethodsShadowItems(boolean methodsShadowItems)
Sets whether methods shadow items in beans.
|
void |
setNullModel(TemplateModel nullModel)
Deprecated.
Changing the
null model can cause a lot of confusion; don't do it. |
void |
setOuterIdentity(ObjectWrapper outerIdentity)
When wrapping an object, the BeansWrapper commonly needs to wrap
"sub-objects", for example each element in a wrapped collection.
|
void |
setSimpleMapWrapper(boolean simpleMapWrapper)
When set to
true , the keys in Map -s won't mix with the method names when looking at them
from templates. |
void |
setStrict(boolean strict)
Specifies if an attempt to read a bean property that doesn't exist in the
wrapped object should throw an
InvalidPropertyException . |
void |
setUseCache(boolean useCache)
Sets whether this wrapper caches the
TemplateModel -s created for the Java objects that has wrapped with
this object wrapper. |
protected String |
toPropertiesString()
Returns the name-value pairs that describe the configuration of this
BeansWrapper ; called from
toString() . |
String |
toString()
Returns the exact class name and the identity hash, also the values of the most often used
BeansWrapper
configuration properties, also if which (if any) shared class introspection cache it uses. |
Object |
tryUnwrapTo(TemplateModel model,
Class targetClass)
Attempts to unwrap a
TemplateModel to a plain Java object that's the instance of the given class (or is
null ). |
Object |
unwrap(TemplateModel model)
Attempts to unwrap a model into underlying object.
|
Object |
unwrap(TemplateModel model,
Class targetClass)
Attempts to unwrap a model into an object of the desired class.
|
TemplateModel |
wrap(Object object)
Wraps the object with a template model that is most specific for the object's
class.
|
TemplateMethodModelEx |
wrap(Object object,
Method method)
Wraps a Java method so that it can be called from templates, without wrapping its parent ("this") object.
|
TemplateHashModel |
wrapAsAPI(Object obj)
Wraps an object to a
TemplateModel that exposes the object's "native" (usually, Java) API. |
void |
writeProtect()
Makes the configuration properties (settings) of this
BeansWrapper object read-only. |
public static final int EXPOSE_ALL
public static final int EXPOSE_SAFE
public static final int EXPOSE_PROPERTIES_ONLY
public static final int EXPOSE_NOTHING
setMethodsShadowItems(boolean)
with false value to
speed up map item retrieval.public BeansWrapper()
BeansWrapperBuilder
or, in rare cases, BeansWrapper(Version)
instead.Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS
.public BeansWrapper(Version incompatibleImprovements)
BeansWrapperBuilder
instead of the public constructors if possible.
The main disadvantage of using the public constructors is that the instances won't share caches. So unless having
a private cache is your goal, don't use them. SeeincompatibleImprovements
- Sets which of the non-backward-compatible improvements should be enabled. Not null
. This version number
is the same as the FreeMarker version number with which the improvements were implemented.
For new projects, it's recommended to set this to the FreeMarker version that's used during the development. For released products that are still actively developed it's a low risk change to increase the 3rd version number further as FreeMarker is updated, but of course you should always check the list of effects below. Increasing the 2nd or 1st version number possibly mean substantial changes with higher risk of breaking the application, but again, see the list of effects below.
The reason it's separate from Configuration.setIncompatibleImprovements(Version)
is that
ObjectWrapper
objects are often shared among multiple Configuration
-s, so the two version
numbers are technically independent. But it's recommended to keep those two version numbers the same.
The changes enabled by incompatibleImprovements
are:
2.3.0: No changes; this is the starting point, the version used in older projects.
2.3.21 (or higher):
Several glitches were fixed in overloaded method selection. This usually just gets
rid of errors (like ambiguity exceptions and numerical precision loses due to bad overloaded method
choices), still, as in some cases the method chosen can be a different one now (that was the point of
the reworking after all), it can mean a change in the behavior of the application. The most important
change is that the treatment of null
arguments were fixed, as earlier they were only seen
applicable to parameters of type Object
. Now null
-s are seen to be applicable to any
non-primitive parameters, and among those the one with the most specific type will be preferred (just
like in Java), which is hence never the one with the Object
parameter type. For more details
about overloaded method selection changes see the version history in the FreeMarker Manual.
Note that the version will be normalized to the lowest version where the same incompatible
BeansWrapper
improvements were already present, so getIncompatibleImprovements()
might returns
a lower version than what you have specified.
protected BeansWrapper(BeansWrapperConfiguration bwConf, boolean writeProtected)
BeansWrapper(BeansWrapperConfiguration, boolean, boolean)
with true
finalizeConstruction
argument.protected BeansWrapper(BeansWrapperConfiguration bwConf, boolean writeProtected, boolean finalizeConstruction)
BeansWrapperConfiguration
specified.writeProtected
- Makes the instance's configuration settings read-only via
WriteProtectable.writeProtect()
; this way it can use the shared class introspection cache.finalizeConstruction
- Decides if the construction is finalized now, or the caller will do some more
adjustments on the instance and then call finalizeConstruction(boolean)
itself.protected void finalizeConstruction(boolean writeProtected)
BeansWrapper(BeansWrapperConfiguration, boolean, boolean)
when
its last argument was false
; makes the instance read-only if necessary, then registers the model
factories in the class introspector. No further changes should be done after calling this, if
writeProtected
was true
.public void writeProtect()
BeansWrapper
object read-only. As changing them
after the object has become visible to multiple threads leads to undefined behavior, it's recommended to call
this when you have finished configuring the object.
Consider using BeansWrapperBuilder
instead, which gives an instance that's already
write protected and also uses some shared caches/pools.
writeProtect
in interface WriteProtectable
public boolean isWriteProtected()
isWriteProtected
in interface WriteProtectable
protected void checkModifiable()
WriteProtectable
, throws IllegalStateException
,
otherwise does nothing.public boolean isStrict()
setStrict(boolean)
public void setStrict(boolean strict)
InvalidPropertyException
.
If this property is false (the default) then an attempt to read a missing bean property is the same as reading an existing bean property whose value is null. The template can't tell the difference, and thus always can use ?default('something') and ?exists and similar built-ins to handle the situation.
If this property is true then an attempt to read a bean propertly in
the template (like myBean.aProperty) that doesn't exist in the bean
object (as opposed to just holding null value) will cause
InvalidPropertyException
, which can't be suppressed in the template
(not even with myBean.noSuchProperty?default('something')). This way
?default('something') and ?exists and similar built-ins can be used to
handle existing properties whose value is null, without the risk of
hiding typos in the property names. Typos will always cause error. But mind you, it
goes against the basic approach of FreeMarker, so use this feature only if you really
know what you are doing.
public void setOuterIdentity(ObjectWrapper outerIdentity)
outerIdentity
- the aggregate ObjectWrapperpublic ObjectWrapper getOuterIdentity()
setOuterIdentity(ObjectWrapper)
public void setSimpleMapWrapper(boolean simpleMapWrapper)
true
, the keys in Map
-s won't mix with the method names when looking at them
from templates. The default is false
for backward-compatibility, but is not recommended.
When this is false
, myMap.foo
or myMap['foo']
either returns the method foo
,
or calls Map.get("foo")
. If both exists (the method and the Map
key), one will hide the other,
depending on the isMethodsShadowItems()
, which default to true
(the method
wins). Some frameworks use this so that you can call myMap.get(nonStringKey)
from templates [*], but it
comes on the cost of polluting the key-set with the method names, and risking methods accidentally hiding
Map
entries (or the other way around). Thus, this setup is not recommended.
(Technical note: Map
-s will be wrapped into MapModel
in this case.)
When this is true
, myMap.foo
or myMap['foo']
always calls Map.get("foo")
.
The methods of the Map
object aren't visible from templates in this case. This, however, spoils the
myMap.get(nonStringKey)
workaround. But now you can use myMap(nonStringKey)
instead, that is, you
can use the map itself as the get
method.
(Technical note: Map
-s will be wrapped into SimpleMapModel
in this case.)
*: For historical reasons, FreeMarker 2.3.X doesn't support non-string keys with the []
operator,
hence the workarounds. This will be likely fixed in FreeMarker 2.4.0. Also note that the method- and
the "field"-namespaces aren't separate in FreeMarker, hence myMap.get
can return the get
method.
public boolean isSimpleMapWrapper()
setSimpleMapWrapper(boolean)
for details.public void setExposureLevel(int exposureLevel)
EXPOSE_SAFE
.exposureLevel
- can be any of the EXPOSE_xxx
constants.public int getExposureLevel()
public void setExposeFields(boolean exposeFields)
exposeFields
- if set to true, public instance fields of classes
that do not have a property getter defined can be accessed directly by
their name. If there is a property getter for a property of the same
name as the field (i.e. getter "getFoo()" and field "foo"), then
referring to "foo" in template invokes the getter. If set to false, no
access to public instance fields of classes is given. Default is false.public boolean isExposeFields()
setExposeFields(boolean)
for details.public MethodAppearanceFineTuner getMethodAppearanceFineTuner()
public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner)
MethodAppearanceFineTuner
for more.public boolean isClassIntrospectionCacheRestricted()
BeansWrapper
-s.
A restricted cache denies certain too "antisocial" operations, like clearClassIntrospecitonCache()
.
The value depends on how the instance
was created; with a public constructor (then this is false
), or with BeansWrapperBuilder
(then it's true
). Note that in the last case it's possible that the introspection cache
will not be actually shared because there's no one to share with, but this will true
even then.public void setMethodsShadowItems(boolean methodsShadowItems)
${object.name}
will first try to locate
a bean method or property with the specified name on the object, and
only if it doesn't find it will it try to call
object.get(name)
, the so-called "generic get method" that
is usually used to access items of a container (i.e. elements of a map).
When set to false, the lookup order is reversed and generic get method
is called first, and only if it returns null is method lookup attempted.public void setDefaultDateType(int defaultDateType)
TemplateDateModel.UNKNOWN
.defaultDateType
- the new default date type.public int getDefaultDateType()
setDefaultDateType(int)
for
details.public void setUseCache(boolean useCache)
TemplateModel
-s created for the Java objects that has wrapped with
this object wrapper. Default is false
.
When set to true
, calling wrap(Object)
multiple times for
the same object will likely return the same model (although there is
no guarantee as the cache items can be cleared any time).public boolean getUseCache()
public void setNullModel(TemplateModel nullModel)
null
model can cause a lot of confusion; don't do it.wrap(Object)
method whenever the wrapped object is
null
. It defaults to null
, which is dealt with quite strictly on engine level, however you can
substitute an arbitrary (perhaps more lenient) model, like an empty string. For proper working, the
nullModel
should be an AdapterTemplateModel
that returns null
for
AdapterTemplateModel.getAdaptedObject(Class)
.public Version getIncompatibleImprovements()
BeansWrapper(Version)
, normalized to the lowest version where a change
has occurred. Thus, this is not necessarily the same version than that was given to the constructor.protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements)
public static final BeansWrapper getDefaultInstance()
BeansWrapperBuilder
instead. The instance returned here is not read-only, so it's
dangerous to use.ObjectWrapper.BEANS_WRAPPER
and this is the sole instance that is used by the JSP adapter.
You can modify the properties of the default instance (caching,
exposure level, null model) to affect its operation. By default, the
default instance is not caching, uses the EXPOSE_SAFE
exposure level, and uses null reference as the null model.public TemplateModel wrap(Object object) throws TemplateModelException
null model
,NumberModel
for it,DateModel
for it,TemplateBooleanModel.TRUE
or
TemplateBooleanModel.FALSE
ArrayModel
for it
MapModel
for it
CollectionModel
for it
IteratorModel
for it
EnumerationModel
for it
StringModel
for it
StringModel
for it.
wrap
in interface ObjectWrapper
object
- The object to wrap into a TemplateModel
. If it already implements TemplateModel
,
it should just return the object as is. If it's null
, the method should return null
(however, BeansWrapper
, has a legacy option for returning a null model object instead, but it's not
a good idea).TemplateModel
wrapper of the object passed in. To support un-wrapping, you may consider the
return value to implement WrapperTemplateModel
and AdapterTemplateModel
.
The default expectation is that the TemplateModel
isn't less thread safe than the wrapped object.
If the ObjectWrapper
returns less thread safe objects, that should be clearly documented, as it
restricts how it can be used, like, then it can't be used to wrap "shared variables"
(Configuration.setSharedVaribles(Map)
).TemplateModelException
public TemplateMethodModelEx wrap(Object object, Method method)
TemplateHashModel
by name. Except, if the wrapped method is overloaded, with this method you
explicitly select a an overload, while otherwise you would get a TemplateMethodModelEx
that selects an
overload each time it's called based on the argument values.object
- The object whose method will be called, or null
if method
is a static method.
This object will be used "as is", like without unwrapping it if it's a TemplateModelAdapter
.method
- The method to call, which must be an (inherited) member of the class of object
, as
described by Method.invoke(Object, Object...)
public TemplateHashModel wrapAsAPI(Object obj) throws TemplateModelException
ObjectWrapperWithAPISupport
TemplateModel
that exposes the object's "native" (usually, Java) API.wrapAsAPI
in interface ObjectWrapperWithAPISupport
obj
- The object for which the API model has to be returned. Shouldn't be null
.TemplateModel
through which the API of the object can be accessed. Can't be null
.TemplateModelException
protected TemplateModel getInstance(Object object, ModelFactory factory)
getModelFactory(Class)
instead. Using this
method will now bypass wrapper caching (if it's enabled) and always
result in creation of a new wrapper. This method will be removed in 2.4object
- The object to wrapfactory
- The factory that wraps the objectprotected ModelFactory getModelFactory(Class clazz)
public Object unwrap(TemplateModel model) throws TemplateModelException
wrap(Object)
method. In addition
it will unwrap arbitrary TemplateNumberModel
instances into
a number, arbitrary TemplateDateModel
instances into a date,
TemplateScalarModel
instances into a String, arbitrary
TemplateBooleanModel
instances into a Boolean, arbitrary
TemplateHashModel
instances into a Map, arbitrary
TemplateSequenceModel
into a List, and arbitrary
TemplateCollectionModel
into a Set. All other objects are
returned unchanged.unwrap
in interface ObjectWrapperAndUnwrapper
null
, if null
is the appropriate Java value to represent
the template model. null
must not be used to indicate an unwrapping failure. It must NOT be
ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS
.TemplateModelException
- if an attempted unwrapping fails.ObjectWrapperAndUnwrapper.tryUnwrapTo(TemplateModel, Class)
public Object unwrap(TemplateModel model, Class targetClass) throws TemplateModelException
wrap(Object)
method. It recognizes a wide range of target classes - all Java built-in
primitives, primitive wrappers, numbers, dates, sets, lists, maps, and
native arrays.model
- the model to unwraptargetClass
- the class of the unwrapped result; Object.class
of we don't know what the expected type is.TemplateModelException
- if an attempted unwrapping fails.tryUnwrapTo(TemplateModel, Class)
public Object tryUnwrapTo(TemplateModel model, Class targetClass) throws TemplateModelException
ObjectWrapperAndUnwrapper
TemplateModel
to a plain Java object that's the instance of the given class (or is
null
).tryUnwrapTo
in interface ObjectWrapperAndUnwrapper
targetClass
- The class that the return value must be an instance of (except when the return value is null
).
Can't be null
; if the caller doesn't care, it should either use {#unwrap(TemplateModel)}, or
Object.class
as the parameter value.targetClass
, or is null
(if null
is the appropriate Java value to represent the template model), or is
ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS
if the unwrapping can't satisfy the targetClass
(nor the
result can be null
). However, ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS
must not be returned if the
targetClass
parameter was Object.class
.TemplateModelException
- If the unwrapping fails for a reason than doesn't fit the meaning of the
ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS
return value.ObjectWrapperAndUnwrapper.unwrap(TemplateModel)
public TemplateHashModel getStaticModels()
statics["java.lang.
System"]. currentTimeMillis()
to call the System.currentTimeMillis()
method.public TemplateHashModel getEnumModels()
statics["java.math.RoundingMode"].UP
to access the
RoundingMode.UP
value.UnsupportedOperationException
- if this method is invoked on a
pre-1.5 JRE, as Java enums aren't supported there.public Object newInstance(Class clazz, List arguments) throws TemplateModelException
clazz
- The class whose constructor we will call.arguments
- The list of TemplateModel
-s to pass to the constructor after unwrapping themTemplateModel
.TemplateModelException
public void removeFromClassIntrospectionCache(Class clazz)
public void clearClassIntrospecitonCache()
Use this if you want to free up memory on the expense of recreating the cache entries for the classes that will be used later in templates.
IllegalStateException
- if isClassIntrospectionCacheRestricted()
is true
.protected void finetuneMethodAppearance(Class clazz, Method m, BeansWrapper.MethodAppearanceDecision decision)
setMethodAppearanceFineTuner(MethodAppearanceFineTuner)
;
no need to extend this class anymore.
Soon this method will be final, so trying to override it will break your app.
Note that if the methodAppearanceFineTuner
property is set to non-null
, this method is not
called anymore.public static void coerceBigDecimals(AccessibleObject callable, Object[] args)
BigDecimal
s in the passed array to the type of
the corresponding formal argument of the method.public static void coerceBigDecimals(Class[] formalTypes, Object[] args)
BigDecimal
s in the passed array to the type of
the corresponding formal argument of the method.public static Object coerceBigDecimal(BigDecimal bd, Class formalType)
public String toString()
BeansWrapper
configuration properties, also if which (if any) shared class introspection cache it uses.protected String toPropertiesString()
BeansWrapper
; called from
toString()
. The expected format is like "foo=bar, baaz=wombat"
. When overriding this, you should
call the super method, and then insert the content before it with a following ", "
, or after it with a
preceding ", "
.