package com.github.czyzby.lml.parser.impl.tag;
import java.io.IOException;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entry;
import com.github.czyzby.kiwi.util.common.Exceptions;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.impl.attribute.table.cell.AbstractCellLmlAttribute;
import com.github.czyzby.lml.parser.impl.tag.macro.AbstractConditionalLmlMacroTag;
import com.github.czyzby.lml.parser.impl.tag.macro.TableCellLmlMacroTag;
import com.github.czyzby.lml.parser.tag.LmlActorBuilder;
import com.github.czyzby.lml.parser.tag.LmlAttribute;
import com.github.czyzby.lml.parser.tag.LmlTag;
import com.github.czyzby.lml.parser.tag.LmlTagProvider;
import com.github.czyzby.lml.util.Lml;
/** Allows to create DTD schema files for LML templates.
*
* @author MJ */
public class Dtd {
protected static final String XML_ELEMENT_REGEX = "[\\w:.-]+";
private boolean displayLogs = true;
private boolean appendComments = true;
/** @param parser contains parsing data. Used to create mock-up actors. The skin MUST be fully loaded and contain
* all used actors' styles for the generator to work properly.
* @return DTD schema file containing all possible tags and their attributes. Any problems with the generation will
* be logged. This is a relatively heavy operation and should be done only during development.
* @see #getDtdSchema(LmlParser, Appendable) */
public static String getSchema(final LmlParser parser) {
final StringBuilder builder = new StringBuilder();
try {
new Dtd().getDtdSchema(parser, builder);
} catch (final IOException exception) {
throw new GdxRuntimeException("Unexpected: unable to append.", exception);
}
return builder.toString();
}
/** Saves DTD schema file containing all possible tags and their attributes. Any problems with the generation will
* be logged. This is a relatively heavy operation and should be done only during development.
*
* @param parser contains parsing data. Used to create mock-up actors. The skin MUST be fully loaded and contain all
* used actors' styles for the generator to work properly.
* @param appendable a reference to the file.
* @see #getDtdSchema(LmlParser, Appendable)
* @see #saveMinifiedSchema(LmlParser, Appendable) */
public static void saveSchema(final LmlParser parser, final Appendable appendable) {
try {
new Dtd().getDtdSchema(parser, appendable);
} catch (final IOException exception) {
throw new GdxRuntimeException("Unable to append to file.", exception);
}
}
/** Saves DTD schema file containing all possible tags and their attributes. Any problems with the generation will
* be logged. This is a relatively heavy operation and should be done only during development. Comments will not be
* appended, which will reduce the size of DTD file.
*
* @param parser contains parsing data. Used to create mock-up actors. The skin MUST be fully loaded and contain all
* used actors' styles for the generator to work properly.
* @param appendable a reference to the file.
* @see #getDtdSchema(LmlParser, Appendable)
* @see #saveSchema(LmlParser, Appendable) */
public static void saveMinifiedSchema(final LmlParser parser, final Appendable appendable) {
try {
new Dtd().setAppendComments(false).getDtdSchema(parser, appendable);
} catch (final IOException exception) {
throw new GdxRuntimeException("Unable to append to file.", exception);
}
}
/** @param displayLogs defaults to true. If set to false, parsing messages will not be shown in the console.
* @return this, for chaining. */
public Dtd setDisplayLogs(final boolean displayLogs) {
this.displayLogs = displayLogs;
return this;
}
/** @param appendComments defaults to true. If false, corresponding Java classes will not be added as comments to
* the DTD file.
* @return this, for chaining. */
public Dtd setAppendComments(final boolean appendComments) {
this.appendComments = appendComments;
return this;
}
/** @param message will be displayed in the console. */
protected void log(final String message) {
if (displayLogs) {
Gdx.app.log(Lml.LOGGER_TAG, message);
}
}
/** Creates DTD schema file containing all possible tags and their attributes. Any problems with the generation will
* be logged. This is a relatively heavy operation and should be done only during development.
*
* @param parser contains parsing data. Used to create mock-up actors. The skin MUST be fully loaded and contain all
* used actors' styles for the generator to work properly.
* @param builder values will be appended to this object.
* @throws IOException when unable to append. */
public void getDtdSchema(final LmlParser parser, final Appendable builder) throws IOException {
appendActorTags(builder, parser);
appendActorAttributes(parser, builder);
appendMacroTags(builder, parser);
appendMacroAttributes(parser, builder);
}
protected void appendDtdElement(final Appendable builder, final String comment, final String name)
throws IOException {
appendDtdElement(builder, comment, Strings.EMPTY_STRING, name);
}
protected void appendDtdElement(final Appendable builder, final String comment, final String prefix,
final String name) throws IOException {
appendDtdElement(builder, comment, prefix, name, "ANY");
}
protected void appendDtdElement(final Appendable builder, final String comment, final String prefix,
final String name, final String type) throws IOException {
if (!name.matches(XML_ELEMENT_REGEX)) {
log("Warning: '" + name + "' tag might contain invalid XML characters.");
}
if (appendComments) {
builder.append("<!-- ").append(comment).append(" -->\n");
}
builder.append("<!ELEMENT ").append(prefix).append(name).append(' ').append(type).append(">\n");
}
protected void appendDtdAttributes(final Appendable builder, final String tagName,
final ObjectMap<String, Object> attributes) throws IOException {
if (appendComments) {
for (final Entry<String, Object> attribute : attributes) {
if (!attribute.key.matches(XML_ELEMENT_REGEX)) {
log("Warning: '" + attribute + "' attribute might contain invalid XML characters.");
}
builder.append("<!-- ").append(attribute.value.getClass().getSimpleName()).append(" -->\n");
builder.append("<!ATTLIST ").append(tagName).append(' ').append(attribute.key)
.append(" CDATA #IMPLIED>\n");
}
return;
}
builder.append("<!ATTLIST ").append(tagName);
for (final Entry<String, Object> attribute : attributes) {
if (!attribute.key.matches(XML_ELEMENT_REGEX)) {
log("Warning: '" + attribute + "' attribute might contain invalid XML characters.");
}
builder.append("\n\t").append(attribute.key).append(" CDATA #IMPLIED");
}
builder.append(">\n");
}
protected void appendActorTags(final Appendable builder, final LmlParser parser) throws IOException {
if (appendComments) {
builder.append("<!-- Actor tags: -->\n");
}
final ObjectMap<String, LmlTagProvider> actorTags = parser.getSyntax().getTags();
for (final Entry<String, LmlTagProvider> actorTag : actorTags) {
appendDtdElement(builder, getTagClassName(actorTag.value), actorTag.key);
}
}
protected String getTagClassName(final LmlTagProvider provider) {
final String providerClass = provider.getClass().getSimpleName();
return providerClass.endsWith("Provider")
? providerClass.substring(0, providerClass.length() - "Provider".length()) : providerClass;
}
@SuppressWarnings("unchecked")
protected void appendActorAttributes(final LmlParser parser, final Appendable builder) throws IOException {
if (appendComments) {
builder.append("<!-- Actor tags' attributes: -->\n");
}
final ObjectMap<String, LmlTagProvider> actorTags = parser.getSyntax().getTags();
for (final Entry<String, LmlTagProvider> actorTag : actorTags) {
final ObjectMap<String, Object> attributes = GdxMaps.newObjectMap();
try {
final LmlTag tag = actorTag.value.create(parser, null, new StringBuilder(actorTag.key));
if (tag.getActor() == null) {
appendNonActorTagAttributes(tag, attributes, parser);
} else {
appendActorTagAttributes(tag, attributes, parser);
}
} catch (final Exception exception) {
Exceptions.ignore(exception);
log("Warning: unable to create an instance of actor mapped to '" + actorTag.key
+ "' tag name with provider: " + actorTag.value
+ ". Attributes list will not be complete. Is the provider properly implemented? Is a default style provided for the selected actor?");
attributes.putAll(
(ObjectMap<String, Object>) (Object) parser.getSyntax().getAttributesForActor(new Actor()));
attributes.putAll((ObjectMap<String, Object>) (Object) parser.getSyntax()
.getAttributesForBuilder(new LmlActorBuilder()));
}
appendDtdAttributes(builder, actorTag.key, attributes);
}
}
@SuppressWarnings("unchecked")
protected void appendNonActorTagAttributes(final LmlTag tag, final ObjectMap<String, Object> attributes,
final LmlParser parser) {
final Object managedObject = tag.getManagedObject();
if (managedObject != null) {
attributes.putAll(
(ObjectMap<String, Object>) (Object) parser.getSyntax().getAttributesForActor(managedObject));
}
}
@SuppressWarnings("unchecked")
protected void appendActorTagAttributes(final LmlTag tag, final ObjectMap<String, Object> attributes,
final LmlParser parser) {
LmlActorBuilder actorBuilder;
final boolean usesAbstractBase = tag instanceof AbstractActorLmlTag;
if (usesAbstractBase) {
actorBuilder = ((AbstractActorLmlTag) tag).getNewInstanceOfBuilder();
} else {
actorBuilder = new LmlActorBuilder();
}
// Appending attributes of component actors:
if (!Lml.DISABLE_COMPONENT_ACTORS_ATTRIBUTE_PARSING && usesAbstractBase
&& ((AbstractActorLmlTag) tag).hasComponentActors()) {
for (final Actor component : ((AbstractActorLmlTag) tag).getComponentActors(tag.getActor())) {
attributes.putAll(
(ObjectMap<String, Object>) (Object) parser.getSyntax().getAttributesForActor(component));
}
}
// Appending managed objects attributes:
if (tag.getManagedObject() != tag.getActor()) {
appendNonActorTagAttributes(tag, attributes, parser);
}
// Appending building attributes:
attributes
.putAll((ObjectMap<String, Object>) (Object) parser.getSyntax().getAttributesForBuilder(actorBuilder));
// Appending regular attributes:
attributes
.putAll((ObjectMap<String, Object>) (Object) parser.getSyntax().getAttributesForActor(tag.getActor()));
}
protected void appendMacroTags(final Appendable builder, final LmlParser parser) throws IOException {
if (appendComments) {
builder.append("<!-- Macro tags: -->\n");
}
final String macroMarker = String.valueOf(parser.getSyntax().getMacroMarker());
if (!macroMarker.matches(XML_ELEMENT_REGEX)) {
log("Error: current macro marker (" + macroMarker
+ ") is an invalid XML character. Override getMacroMarker in your current LmlSyntax implementation and provide a correct character to create valid DTD file.");
}
final ObjectMap<String, LmlTagProvider> macroTags = parser.getSyntax().getMacroTags();
for (final Entry<String, LmlTagProvider> macroTag : macroTags) {
appendDtdElement(builder, getTagClassName(macroTag.value), macroMarker, macroTag.key);
// If the tag is conditional, it should provide an extra name:else tag:
try {
final LmlTag tag = macroTag.value.create(parser, null, new StringBuilder(macroTag.key));
if (tag instanceof AbstractConditionalLmlMacroTag) {
appendDtdElement(builder, "'Else' helper tag of: " + macroTag.key, macroMarker,
macroTag.key + AbstractConditionalLmlMacroTag.ELSE_SUFFIX, "EMPTY");
}
} catch (final Exception expected) {
// Tag might need a parent or additional attributes and cannot be checked. It's OK.
Exceptions.ignore(expected);
log("Unable to create a macro tag instance using: " + macroTag.value.getClass().getSimpleName());
}
}
}
protected void appendMacroAttributes(final LmlParser parser, final Appendable builder) throws IOException {
if (appendComments) {
builder.append("<!-- Expected macro tags' attributes: -->\n");
}
final String macroMarker = String.valueOf(parser.getSyntax().getMacroMarker());
final ObjectMap<String, LmlTagProvider> macroTags = parser.getSyntax().getMacroTags();
for (final Entry<String, LmlTagProvider> macroTag : macroTags) {
try {
final LmlTag tag = macroTag.value.create(parser, null, new StringBuilder(macroTag.key));
if (tag instanceof TableCellLmlMacroTag) {
// Special case: listing all cell attributes:
appendTableDefaultsMacro(parser, builder, macroMarker, macroTag, tag);
} else if (tag instanceof AbstractMacroLmlTag) {
final String[] attributeNames = ((AbstractMacroLmlTag) tag).getExpectedAttributes();
if (attributeNames == null || attributeNames.length == 0) {
continue;
}
final ObjectMap<String, Object> attributes = GdxMaps.newObjectMap();
for (final String attributeName : attributeNames) {
attributes.put(attributeName, tag);
}
appendDtdAttributes(builder, macroMarker + macroTag.key, attributes);
}
} catch (final Exception expected) {
// Tag might need a parent or additional attributes and cannot be checked. It's OK.
Exceptions.ignore(expected);
}
}
}
private void appendTableDefaultsMacro(final LmlParser parser, final Appendable builder, final String macroMarker,
final Entry<String, LmlTagProvider> macroTag, final LmlTag tag) throws IOException {
final Actor mockUp = new Actor();
final ObjectMap<String, Object> attributes = GdxMaps.newObjectMap();
for (final Entry<String, LmlAttribute<?>> attribute : parser.getSyntax().getAttributesForActor(mockUp)) {
if (attribute.value instanceof AbstractCellLmlAttribute) {
attributes.put(attribute.key, attribute.value);
}
}
final String[] attributeNames = ((AbstractMacroLmlTag) tag).getExpectedAttributes();
if (attributeNames != null && attributeNames.length > 0) {
for (final String attribute : attributeNames) {
attributes.put(attribute, tag);
}
}
appendDtdAttributes(builder, macroMarker + macroTag.key, attributes);
}
}