commit 5087e092c1dd38632ed610bf53adbaaedfbc81cb Author: Walter Oggioni Date: Sun Sep 9 13:37:13 2018 +0100 initial commit diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..2c6b443 --- /dev/null +++ b/build.sbt @@ -0,0 +1,33 @@ +name := "worth" + +organization := "org.oggio88" + +version := "1.0" +resolvers += Resolver.mavenLocal +scalaVersion := "2.12.6" + +scalacOptions ++= Seq( + "-unchecked", + "-deprecation", + "-language:_", + "-opt:l:inline", "-opt-inline-from", + "-target:jvm-1.8", + "-encoding", "UTF-8" +) + +git.useGitDescribe := true +fork := true +//javaOptions in Test += "-Dorg.oggio88.javason.value.ObjectValue.listBasedImplementation=true" +javaOptions in Test += "-Xmx6G" +//scalafmtOnCompile := true +libraryDependencies += "org.projectlombok" % "lombok" % "1.18.2" +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test" +libraryDependencies += "com.fasterxml.jackson.core" % "jackson-databind" % "2.9.6" % "test" + +libraryDependencies += "org.antlr" % "antlr4" % "4.7.1" % "compile" +libraryDependencies += "org.antlr" % "antlr4-runtime" % "4.7.1" % "test" +libraryDependencies += "org.tukaani" % "xz" % "1.8" % "test" + +artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) => + artifact.name + "-" + module.revision + "." + artifact.extension +} diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..f67cbbe --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,9 @@ +//addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.2") +addSbtPlugin("com.dwijnand" % "sbt-travisci" % "1.1.1") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") +addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") +//addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.9.3") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "4.1.0") +addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.3") +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.6") diff --git a/src/main/java/org/oggio88/worth/buffer/CircularBuffer.java b/src/main/java/org/oggio88/worth/buffer/CircularBuffer.java new file mode 100644 index 0000000..bab82db --- /dev/null +++ b/src/main/java/org/oggio88/worth/buffer/CircularBuffer.java @@ -0,0 +1,38 @@ +package org.oggio88.worth.buffer; + +import lombok.SneakyThrows; + +import java.io.Reader; + +public class CircularBuffer { + + private int[] buffer; + private Reader reader; + private int delta = 0, cursor = 0; + + public CircularBuffer(Reader reader, int size) { + this.reader = reader; + buffer = new int[size]; + } + + @SneakyThrows + public int next() { + if (delta < 0) + return buffer[Math.floorMod(cursor + delta++, buffer.length)]; + else { + int result = reader.read(); + if (result < 0) return result; + buffer[cursor] = result; + cursor = (cursor + 1) % buffer.length; + return result; + } + } + + public int prev() { + return buffer[cursor + --delta >= 0 ? cursor + delta : cursor + delta + buffer.length]; + } + + public int size() { + return buffer.length; + } +} diff --git a/src/main/java/org/oggio88/worth/exception/IOException.java b/src/main/java/org/oggio88/worth/exception/IOException.java new file mode 100644 index 0000000..1603183 --- /dev/null +++ b/src/main/java/org/oggio88/worth/exception/IOException.java @@ -0,0 +1,7 @@ +package org.oggio88.worth.exception; + +public class IOException extends WorthException { + public IOException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/oggio88/worth/exception/NotImplementedException.java b/src/main/java/org/oggio88/worth/exception/NotImplementedException.java new file mode 100644 index 0000000..66f889b --- /dev/null +++ b/src/main/java/org/oggio88/worth/exception/NotImplementedException.java @@ -0,0 +1,7 @@ +package org.oggio88.worth.exception; + +public class NotImplementedException extends WorthException { + public NotImplementedException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/oggio88/worth/exception/ParseException.java b/src/main/java/org/oggio88/worth/exception/ParseException.java new file mode 100644 index 0000000..8a1275e --- /dev/null +++ b/src/main/java/org/oggio88/worth/exception/ParseException.java @@ -0,0 +1,7 @@ +package org.oggio88.worth.exception; + +public class ParseException extends WorthException { + public ParseException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/oggio88/worth/exception/TypeException.java b/src/main/java/org/oggio88/worth/exception/TypeException.java new file mode 100644 index 0000000..b3cdfe2 --- /dev/null +++ b/src/main/java/org/oggio88/worth/exception/TypeException.java @@ -0,0 +1,7 @@ +package org.oggio88.worth.exception; + +public class TypeException extends WorthException { + public TypeException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/oggio88/worth/exception/WorthException.java b/src/main/java/org/oggio88/worth/exception/WorthException.java new file mode 100644 index 0000000..23c6eef --- /dev/null +++ b/src/main/java/org/oggio88/worth/exception/WorthException.java @@ -0,0 +1,7 @@ +package org.oggio88.worth.exception; + +public class WorthException extends RuntimeException { + public WorthException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/oggio88/worth/serialization/ValueDumper.java b/src/main/java/org/oggio88/worth/serialization/ValueDumper.java new file mode 100644 index 0000000..a2f7f63 --- /dev/null +++ b/src/main/java/org/oggio88/worth/serialization/ValueDumper.java @@ -0,0 +1,105 @@ +package org.oggio88.worth.serialization; + +import lombok.RequiredArgsConstructor; +import org.oggio88.worth.exception.NotImplementedException; +import org.oggio88.worth.value.ArrayValue; +import org.oggio88.worth.value.ObjectValue; +import org.oggio88.worth.xface.Dumper; +import org.oggio88.worth.xface.Value; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.Map; +import java.util.Stack; + +public abstract class ValueDumper implements Dumper { + + @RequiredArgsConstructor + protected static class StackLevel { + public int index = 0; + public final Value value; + } + + protected static class ArrayStackLevel extends StackLevel implements Iterator { + private final Iterator iterator = value.asArray().iterator(); + + @Override + public Value next() { + ++index; + return iterator.next(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + public ArrayStackLevel(ArrayValue value) { + super(value); + } + } + + protected static class ObjectStackLevel extends StackLevel implements Iterator> { + private final Iterator> iterator = value.asObject().entrySet().iterator(); + + @Override + public Map.Entry next() { + ++index; + return iterator.next(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + public ObjectStackLevel(ObjectValue value) { + super(value); + } + } + + + protected Stack stack; + + protected ValueDumper() { + stack = new Stack<>(); + } + + @Override + public void dump(Value value, OutputStream stream) { + throw new NotImplementedException("Method not implemented"); + } + + @Override + public void dump(Value value, Writer writer) { + throw new NotImplementedException("Method not implemented"); + } + + @Override + public void dump(Value value, OutputStream stream, Charset encoding) { + dump(value, new OutputStreamWriter(stream, encoding)); + } + + protected abstract void beginObject(); + + protected abstract void endObject(); + + protected abstract void beginArray(); + + protected abstract void endArray(); + + protected abstract void objectKey(String key); + + protected abstract void stringValue(String value); + + protected abstract void integerValue(long value); + + protected abstract void floatValue(double value); + + protected abstract void booleanValue(boolean value); + + protected abstract void nullValue(); +} diff --git a/src/main/java/org/oggio88/worth/serialization/ValueParser.java b/src/main/java/org/oggio88/worth/serialization/ValueParser.java new file mode 100644 index 0000000..15e198c --- /dev/null +++ b/src/main/java/org/oggio88/worth/serialization/ValueParser.java @@ -0,0 +1,116 @@ +package org.oggio88.worth.serialization; + +import lombok.RequiredArgsConstructor; +import org.oggio88.worth.exception.NotImplementedException; +import org.oggio88.worth.utils.WorthUtils; +import org.oggio88.worth.value.ArrayValue; +import org.oggio88.worth.value.BooleanValue; +import org.oggio88.worth.value.FloatValue; +import org.oggio88.worth.value.IntegerValue; +import org.oggio88.worth.value.ObjectValue; +import org.oggio88.worth.value.StringValue; +import org.oggio88.worth.xface.Parser; +import org.oggio88.worth.xface.Value; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.Stack; + +public class ValueParser implements Parser { + + @RequiredArgsConstructor + protected static class StackLevel { + public final Value value; + } + + protected static class ArrayStackLevel extends StackLevel { + public ArrayStackLevel() { + super(new ArrayValue()); + } + } + + protected static class ObjectStackLevel extends StackLevel { + public String currentKey; + + public ObjectStackLevel() { + super(ObjectValue.newInstance()); + } + } + + protected Stack stack; + + private void add2Last(Value value) { + StackLevel last = stack.lastElement(); + ArrayStackLevel asl; + ObjectStackLevel osl; + if ((asl = WorthUtils.dynamicCast(last, ArrayStackLevel.class)) != null) + asl.value.add(value); + else if ((osl = WorthUtils.dynamicCast(last, ObjectStackLevel.class)) != null) { + osl.value.put(osl.currentKey, value); + osl.currentKey = null; + } + } + + protected ValueParser() { + stack = new Stack<>(); + stack.push(new ArrayStackLevel()); + } + + @Override + public Value parse(InputStream is) { + throw new NotImplementedException("Method not implemented"); + } + + @Override + public Value parse(Reader reader) { + throw new NotImplementedException("Method not implemented"); + } + + @Override + public Value parse(InputStream stream, Charset encoding) { + return parse(new InputStreamReader(stream, encoding)); + } + + protected void beginObject() { + stack.push(new ObjectStackLevel()); + } + + protected void endObject() { + add2Last(stack.pop().value); + } + + protected void beginArray() { + stack.push(new ArrayStackLevel()); + } + + protected void endArray() { + add2Last(stack.pop().value); + } + + protected void objectKey(String key) { + ObjectStackLevel osl = (ObjectStackLevel) stack.lastElement(); + osl.currentKey = key; + } + + protected void stringValue(String value) { + add2Last(new StringValue(value)); + } + + protected void integerValue(long value) { + add2Last(new IntegerValue(value)); + } + + protected void floatValue(double value) { + add2Last(new FloatValue(value)); + } + + protected void booleanValue(boolean value) { + add2Last(new BooleanValue(value)); + } + + protected void nullValue() { + add2Last(Value.Null); + } +} diff --git a/src/main/java/org/oggio88/worth/serialization/json/JSONDumper.java b/src/main/java/org/oggio88/worth/serialization/json/JSONDumper.java new file mode 100644 index 0000000..fd31650 --- /dev/null +++ b/src/main/java/org/oggio88/worth/serialization/json/JSONDumper.java @@ -0,0 +1,183 @@ +package org.oggio88.worth.serialization.json; + +import lombok.SneakyThrows; +import org.oggio88.worth.serialization.ValueDumper; +import org.oggio88.worth.value.ArrayValue; +import org.oggio88.worth.value.ObjectValue; +import org.oggio88.worth.xface.Dumper; +import org.oggio88.worth.xface.Value; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Map; +import java.util.function.Consumer; + +import static org.oggio88.worth.utils.WorthUtils.dynamicCast; + +public class JSONDumper extends ValueDumper { + + public static Dumper newInstance() { + return new JSONDumper(); + } + + protected Writer writer; + + @Override + public void dump(Value value, OutputStream stream) { + dump(value, new OutputStreamWriter(stream)); + } + + @Override + @SneakyThrows + public void dump(Value value, Writer writer) { + this.writer = writer; + final Consumer handle_value = (v) -> { + switch (v.type()) { + case NULL: + nullValue(); + break; + case BOOLEAN: + booleanValue(v.asBoolean()); + break; + case INTEGER: + integerValue(v.asInteger()); + break; + case DOUBLE: + floatValue(v.asFloat()); + break; + case STRING: + stringValue(v.asString()); + break; + case ARRAY: + stack.push(new ArrayStackLevel(dynamicCast(v, ArrayValue.class))); + beginArray(); + break; + case OBJECT: + stack.push(new ObjectStackLevel(dynamicCast(v, ObjectValue.class))); + beginObject(); + break; + } + }; + + handle_value.accept(value); + while (stack.size() > 0) { + StackLevel last = stack.lastElement(); + ArrayStackLevel arrayStackLevel; + ObjectStackLevel objectStackLevel; + if ((arrayStackLevel = dynamicCast(last, ArrayStackLevel.class)) != null) { + if (arrayStackLevel.hasNext()) { + if (arrayStackLevel.index > 0) { + writer.write(","); + } + handle_value.accept(arrayStackLevel.next()); + } else { + endArray(); + stack.pop(); + } + } else if ((objectStackLevel = dynamicCast(last, ObjectStackLevel.class)) != null) { + if (objectStackLevel.hasNext()) { + if (objectStackLevel.index > 0) { + writer.write(","); + } + Map.Entry entry = objectStackLevel.next(); + objectKey(entry.getKey()); + writer.write(":"); + handle_value.accept(entry.getValue()); + } else { + endObject(); + stack.pop(); + } + } + } + this.writer.flush(); + this.writer = null; + } + + @Override + @SneakyThrows + protected void beginObject() { + this.writer.write("{"); + } + + @Override + @SneakyThrows + protected void endObject() { + this.writer.write("}"); + } + + @Override + @SneakyThrows + protected void beginArray() { + this.writer.write("["); + } + + @Override + @SneakyThrows + protected void endArray() { + this.writer.write("]"); + } + + @Override + @SneakyThrows + protected void objectKey(String key) { + this.writer.write("\"" + key + "\""); + } + + @Override + @SneakyThrows + protected void stringValue(String value) { + StringBuilder sb = new StringBuilder(); + for (char c : value.toCharArray()) { + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\r': + sb.append("\\r"); + break; + case '\n': + sb.append("\\n"); + break; + case '\t': + sb.append("\\t"); + break; + case '\\': + sb.append("\\\\"); + break; + default: { + if (c < 128) + sb.append(c); + else { + sb.append("\\u").append(String.format("%04X", (int) c)); + } + } + } + } + this.writer.write("\"" + sb.toString() + "\""); + } + + @Override + @SneakyThrows + protected void integerValue(long value) { + this.writer.write(Long.toString(value)); + } + + @Override + @SneakyThrows + protected void floatValue(double value) { + this.writer.write(Double.toString(value)); + } + + @Override + @SneakyThrows + protected void booleanValue(boolean value) { + this.writer.write(Boolean.toString(value)); + } + + @Override + @SneakyThrows + protected void nullValue() { + this.writer.write("null"); + } +} diff --git a/src/main/java/org/oggio88/worth/serialization/json/JSONParser.java b/src/main/java/org/oggio88/worth/serialization/json/JSONParser.java new file mode 100644 index 0000000..f7faeae --- /dev/null +++ b/src/main/java/org/oggio88/worth/serialization/json/JSONParser.java @@ -0,0 +1,227 @@ +package org.oggio88.worth.serialization.json; + +import lombok.SneakyThrows; +import org.oggio88.worth.buffer.CircularBuffer; +import org.oggio88.worth.exception.IOException; +import org.oggio88.worth.exception.NotImplementedException; +import org.oggio88.worth.exception.ParseException; +import org.oggio88.worth.serialization.ValueParser; +import org.oggio88.worth.utils.WorthUtils; +import org.oggio88.worth.xface.Parser; +import org.oggio88.worth.xface.Value; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.function.Function; + +public class JSONParser extends ValueParser { + + private int currentLine = 1, currentColumn = 1; + + private static boolean isBlank(int c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + private static boolean isDecimal(int c) { + return c >= '0' && c <= '9' || c == '+' || c == '-' || c == '.' || c == 'e'; + } + + private static int parseHex(CircularBuffer circularBuffer) { + int result = 0; + while (true) { + int c = circularBuffer.next(); + if (c >= '0' && c <= '9') { + result = result << 4; + result += (c - '0'); + } else if (c >= 'a' && c <= 'f') { + result = result << 4; + result += 10 + (c - 'a'); + } else if (c >= 'A' && c <= 'F') { + result = result << 4; + result += 10 + (c - 'A'); + } else { + circularBuffer.prev(); + break; + } + } + return result; + } + + private final void parseNumber(CircularBuffer circularBuffer) { + StringBuilder sb = new StringBuilder(); + while (true) { + int b = circularBuffer.next(); + if (isDecimal(b)) { + sb.appendCodePoint(b); + } else { + circularBuffer.prev(); + break; + } + } + String text = sb.toString(); + if (text.indexOf('.') > 0) { + floatValue(Double.valueOf(text)); + } else { + integerValue(Long.valueOf(text)); + } + } + + private final String readString(CircularBuffer circularBuffer) { + StringBuilder sb = new StringBuilder(); + boolean escape = false; + while (true) { + int c = circularBuffer.next(); + if (c < 0) { + circularBuffer.prev(); + break; + } else if (escape) { + switch (c) { + case '"': + sb.append('\"'); + break; + case 'r': + sb.append('\r'); + break; + case 'n': + sb.append('\n'); + break; + case 't': + sb.append('\t'); + break; + case '\\': + sb.append('\\'); + break; + case 'u': + int codePoint = parseHex(circularBuffer); + sb.appendCodePoint(codePoint); + break; + default: + throw error(ParseException::new, "Unrecognized escape sequence '\\%c'", c); + } + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == '\"') { + break; + } else { + sb.appendCodePoint(c); + } + } + return sb.toString(); + } + + private final void consumeExpected(CircularBuffer circularBuffer, String expected, String errorMessage) { + for (int i = 0; i < expected.length(); i++) { + int c = circularBuffer.next(); + if (c < 0) { + throw error(IOException::new, "Unexpected end of stream"); + } + if (c != expected.codePointAt(i)) throw error(ParseException::new, errorMessage); + } + } + + private T error(Function constructor, String fmt, Object... args) { + return constructor.apply( + String.format("Error at line %d column %d: %s", + currentLine, currentColumn, String.format(fmt, args))); + } + + public static Parser newInstance() { + return new JSONParser(); + } + + @Override + public Value parse(InputStream stream) { + return parse(new InputStreamReader(stream)); + } + + @Override + @SneakyThrows + public Value parse(Reader reader) { + final CircularBuffer circularBuffer = new CircularBuffer(reader, 8) { + + @Override + public int next() { + int result = super.next(); + if (result == '\n') { + ++currentLine; + currentColumn = 1; + } else { + ++currentColumn; + } + return result; + } + + @Override + public int prev() { + int result = super.prev(); + if (result == '\n') { + --currentLine; + } else { + --currentColumn; + } + return result; + } + }; + + try { + while (true) { + int c = circularBuffer.next(); + if (c == -1) { + break; + } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + continue; + } else if (c == '{') { + beginObject(); + } else if (c == '}') { + endObject(); + } else if (c == '[') { + beginArray(); + } else if (c == ']') { + endArray(); + } else if (isDecimal(c)) { + circularBuffer.prev(); + try { + parseNumber(circularBuffer); + } catch (NumberFormatException nfe) { + + } + } else if (c == '\"') { + String text = readString(circularBuffer); + ObjectStackLevel osl; + if ((osl = WorthUtils.dynamicCast(stack.lastElement(), ObjectStackLevel.class)) != null && osl.currentKey == null) { + objectKey(text); + } else { + stringValue(text); + } + } else if (c == 't') { + consumeExpected(circularBuffer, "rue", "Unrecognized boolean value"); + booleanValue(true); + } else if (c == 'f') { + consumeExpected(circularBuffer, "alse", "Unrecognized boolean value"); + booleanValue(false); + } else if (c == 'n') { + consumeExpected(circularBuffer, "ull", "Unrecognized null value"); + nullValue(); + } + } + if (stack.size() > 1) { + char c; + if (stack.lastElement() instanceof ArrayStackLevel) { + c = ']'; + } else if (stack.lastElement() instanceof ObjectStackLevel) { + c = '}'; + } else { + throw new NotImplementedException("This should never happen"); + } + throw error(ParseException::new, "Missing '%c' token", c); + } + return WorthUtils.dynamicCast(stack.lastElement(), ArrayStackLevel.class).value.get(0); + } catch (NumberFormatException e) { + throw error(ParseException::new, e.getMessage()); + } finally { + stack.clear(); + } + } +} diff --git a/src/main/java/org/oggio88/worth/utils/WorthUtils.java b/src/main/java/org/oggio88/worth/utils/WorthUtils.java new file mode 100644 index 0000000..d63ab77 --- /dev/null +++ b/src/main/java/org/oggio88/worth/utils/WorthUtils.java @@ -0,0 +1,26 @@ +package org.oggio88.worth.utils; + +import lombok.SneakyThrows; + +import java.util.concurrent.Callable; + +public class WorthUtils { + + @SneakyThrows + public static T uncheckCall(final Callable callable) { + return callable.call(); + } + + public static T dynamicCast(final Object o, final Class cls) { + if (cls.isInstance(o)) { + return (T) o; + } else { + return null; + } + } + + public static boolean equalsNullSafe(Object o1, Object o2) { + if (o1 == null) return o2 == null; + else return o1.equals(o2); + } +} diff --git a/src/main/java/org/oggio88/worth/value/ArrayValue.java b/src/main/java/org/oggio88/worth/value/ArrayValue.java new file mode 100644 index 0000000..424fc2d --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/ArrayValue.java @@ -0,0 +1,69 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import org.oggio88.worth.xface.Value; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +@EqualsAndHashCode +public class ArrayValue implements Value, Iterable { + + private final List value; + + public ArrayValue() { + this.value = new ArrayList(); + } + + public ArrayValue(List value) { + this.value = value; + } + + @Override + public Type type() { + return Type.ARRAY; + } + + @Override + public void add(Value value) { + this.value.add(value); + } + + @Override + public Value get(int index) { + return value.get(index); + } + + @Override + public Value pop() { + Value last = tail(); + value.remove(value.size() - 1); + return last; + } + + @Override + public Value head() { + return value.get(0); + } + + @Override + public Value tail() { + return value.get(value.size() - 1); + } + + @Override + public List asArray() { + return value; + } + + + @Override + public Iterator iterator() { + return value.iterator(); + } + + public int size() { + return value.size(); + } +} diff --git a/src/main/java/org/oggio88/worth/value/BooleanValue.java b/src/main/java/org/oggio88/worth/value/BooleanValue.java new file mode 100644 index 0000000..d2af235 --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/BooleanValue.java @@ -0,0 +1,24 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import org.oggio88.worth.xface.Value; + +@EqualsAndHashCode +public class BooleanValue implements Value { + + private final boolean value; + + public BooleanValue(boolean value) { + this.value = value; + } + + @Override + public Type type() { + return Type.BOOLEAN; + } + + @Override + public boolean asBoolean() { + return value; + } +} diff --git a/src/main/java/org/oggio88/worth/value/FloatValue.java b/src/main/java/org/oggio88/worth/value/FloatValue.java new file mode 100644 index 0000000..84a5425 --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/FloatValue.java @@ -0,0 +1,24 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import org.oggio88.worth.xface.Value; + +@EqualsAndHashCode +public class FloatValue implements Value { + + private final double value; + + public FloatValue(double value) { + this.value = value; + } + + @Override + public Type type() { + return Type.DOUBLE; + } + + @Override + public double asFloat() { + return value; + } +} diff --git a/src/main/java/org/oggio88/worth/value/IntegerValue.java b/src/main/java/org/oggio88/worth/value/IntegerValue.java new file mode 100644 index 0000000..366a609 --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/IntegerValue.java @@ -0,0 +1,24 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import org.oggio88.worth.xface.Value; + +@EqualsAndHashCode +public class IntegerValue implements Value { + + private final long value; + + public IntegerValue(long value) { + this.value = value; + } + + @Override + public Type type() { + return Type.INTEGER; + } + + @Override + public long asInteger() { + return value; + } +} diff --git a/src/main/java/org/oggio88/worth/value/NullValue.java b/src/main/java/org/oggio88/worth/value/NullValue.java new file mode 100644 index 0000000..b947676 --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/NullValue.java @@ -0,0 +1,13 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import org.oggio88.worth.xface.Value; + +@EqualsAndHashCode +public class NullValue implements Value { + + @Override + public Type type() { + return Type.NULL; + } +} diff --git a/src/main/java/org/oggio88/worth/value/ObjectValue.java b/src/main/java/org/oggio88/worth/value/ObjectValue.java new file mode 100644 index 0000000..9aecc9e --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/ObjectValue.java @@ -0,0 +1,185 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.oggio88.worth.xface.Value; + +import java.util.*; + +import static org.oggio88.worth.utils.WorthUtils.equalsNullSafe; + + +public interface ObjectValue extends Value, Iterable> { + + boolean listBasedImplementation = Boolean.valueOf( + System.getProperty("org.oggio88.javason.value.ObjectValue.listBasedImplementation", "false")); + boolean preserveKeyOrder = Boolean.valueOf( + System.getProperty("org.oggio88.javason.value.MapObjectValue.preserveKeyOrder", "false")); + + static ObjectValue newInstance() { + if (listBasedImplementation) { + return new MapObjectValue(); + } else { + return new MapObjectValue(); + } + } + + @Override + default Type type() { + return Type.OBJECT; + } +} + +final class ObjectEntry implements Map.Entry { + private final K key; + private V value; + + public ObjectEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + V old = this.value; + this.value = value; + return old; + } +} + +@EqualsAndHashCode +class MapObjectValue implements ObjectValue { + + private final Map value; + + public MapObjectValue() { + this.value = ObjectValue.preserveKeyOrder ? new LinkedHashMap() : new HashMap(); + } + + public MapObjectValue(Map value) { + this.value = value; + } + + @Override + public Map asObject() { + return value; + } + + @Override + public Value get(String key) { + Value result = value.get(key); + if (result == null) { + result = Value.Null; + value.put(key, result); + } + return result; + } + + @Override + public Value getOrDefault(String key, Value defaultValue) { + if (value.containsKey(key)) + return value.get(key); + else + return defaultValue; + } + + @Override + public Value getOrPut(String key, Value value2Put) { + if (value.containsKey(key)) + return value.get(key); + else { + put(key, value2Put); + return value2Put; + } + } + + @Override + public void put(String key, Value value2Put) { + this.value.put(key, value2Put); + } + + + @Override + public boolean has(String key) { + return value.containsKey(key); + } + + @Override + public Iterator> iterator() { + return value.entrySet().iterator(); + } +} + +@NoArgsConstructor +@EqualsAndHashCode +class ListObjectValue implements ObjectValue { + + private final List> value = new ArrayList(); + + public ListObjectValue(Map map) { + this.value.addAll(map.entrySet()); + } + + @Override + public Map asObject() { + Map result = preserveKeyOrder ? new LinkedHashMap() : new HashMap(); + for (Map.Entry entry : value) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } + + @Override + public Value get(String key) { + for (Map.Entry entry : value) { + if(equalsNullSafe(entry.getKey(), key)) return entry.getValue(); + } + return Value.Null; + } + + @Override + public Value getOrDefault(String key, Value defaultValue) { + for (Map.Entry entry : value) { + if(equalsNullSafe(entry.getKey(), key)) return entry.getValue(); + } + return defaultValue; + } + + @Override + public Value getOrPut(String key, Value value2Put) { + for (Map.Entry entry : value) { + if(equalsNullSafe(entry.getKey(), key)) return entry.getValue(); + } + put(key, value2Put); + return value2Put; + } + + @Override + public void put(String key, Value value2Put) { + value.add(new ObjectEntry(key, value2Put)); + } + + + @Override + public boolean has(String key) { + for (Map.Entry entry : value) { + if(equalsNullSafe(entry.getKey(), key)) return true; + } + return false; + } + + @Override + public Iterator> iterator() { + return value.iterator(); + } +} diff --git a/src/main/java/org/oggio88/worth/value/StringValue.java b/src/main/java/org/oggio88/worth/value/StringValue.java new file mode 100644 index 0000000..8f98e0f --- /dev/null +++ b/src/main/java/org/oggio88/worth/value/StringValue.java @@ -0,0 +1,25 @@ +package org.oggio88.worth.value; + +import lombok.EqualsAndHashCode; +import org.oggio88.worth.xface.Value; + +@EqualsAndHashCode +public class StringValue implements Value { + + private final String value; + + public StringValue(String value) + { + this.value = value; + } + + @Override + public Type type() { + return Type.STRING; + } + + @Override + public String asString() { + return value; + } +} diff --git a/src/main/java/org/oggio88/worth/xface/Dumper.java b/src/main/java/org/oggio88/worth/xface/Dumper.java new file mode 100644 index 0000000..073b562 --- /dev/null +++ b/src/main/java/org/oggio88/worth/xface/Dumper.java @@ -0,0 +1,13 @@ +package org.oggio88.worth.xface; + +import java.io.OutputStream; +import java.io.Writer; +import java.nio.charset.Charset; + +public interface Dumper { + void dump(Value value, OutputStream is); + + void dump(Value value, Writer reader); + + void dump(Value value, OutputStream stream, Charset encoding); +} diff --git a/src/main/java/org/oggio88/worth/xface/Parser.java b/src/main/java/org/oggio88/worth/xface/Parser.java new file mode 100644 index 0000000..3aff1ee --- /dev/null +++ b/src/main/java/org/oggio88/worth/xface/Parser.java @@ -0,0 +1,14 @@ +package org.oggio88.worth.xface; + +import java.io.InputStream; +import java.io.Reader; +import java.nio.charset.Charset; + +public interface Parser { + + Value parse(InputStream is); + + Value parse(Reader reader); + + Value parse(InputStream stream, Charset encoding); +} diff --git a/src/main/java/org/oggio88/worth/xface/Value.java b/src/main/java/org/oggio88/worth/xface/Value.java new file mode 100644 index 0000000..fd4cdbc --- /dev/null +++ b/src/main/java/org/oggio88/worth/xface/Value.java @@ -0,0 +1,82 @@ +package org.oggio88.worth.xface; + +import org.oggio88.worth.exception.TypeException; +import org.oggio88.worth.value.NullValue; + +import java.util.List; +import java.util.Map; + +public interface Value { + + Value Null = new NullValue(); + + enum Type { + OBJECT, ARRAY, STRING, DOUBLE, INTEGER, BOOLEAN, NULL + } + + Type type(); + + default boolean asBoolean() { + throw new TypeException("Not a boolean"); + } + + default long asInteger() { + throw new TypeException("Not an integer"); + } + + default double asFloat() { + throw new TypeException("Not a float"); + } + + default String asString() { + throw new TypeException("Not a String"); + } + + default List asArray() { + throw new TypeException("Not an array"); + } + + default Map asObject() { + throw new TypeException("Not an object"); + } + + default void add(Value value) { + throw new TypeException("Not an array"); + } + + default Value pop() { + throw new TypeException("Not an array"); + } + + default Value head() { + throw new TypeException("Not an array"); + } + + default Value tail() { + throw new TypeException("Not an array"); + } + + default Value get(int index) { + throw new TypeException("Not an array"); + } + + default void put(String key, Value value) { + throw new TypeException("Not an object"); + } + + default Value get(String key) { + throw new TypeException("Not an object"); + } + + default Value getOrDefault(String key, Value defaultValue) { + throw new TypeException("Not an object"); + } + + default Value getOrPut(String key, Value value2Put) { + throw new TypeException("Not an object"); + } + + default boolean has(String key) { + throw new TypeException("Not an object"); + } +} diff --git a/src/test/java/org/oggio88/worth/buffer/CircularBufferTest.java b/src/test/java/org/oggio88/worth/buffer/CircularBufferTest.java new file mode 100644 index 0000000..48bf6aa --- /dev/null +++ b/src/test/java/org/oggio88/worth/buffer/CircularBufferTest.java @@ -0,0 +1,36 @@ +package org.oggio88.worth.buffer; + +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.Random; + +public class CircularBufferTest { + + @Test + @SneakyThrows + public void test() { + MessageDigest streamDigest = MessageDigest.getInstance("MD5"), outputDigest = MessageDigest.getInstance("MD5"); + InputStream is = new DigestInputStream(getClass().getResourceAsStream("/logging.properties"), streamDigest); + CircularBuffer cb = new CircularBuffer(new InputStreamReader(is), 32); + Random rand = new Random(); + while (true) { + int b = cb.next(); + if (b < 0) break; + if (rand.nextInt() % 2 == 0) { + cb.prev(); + } else { + char c = (char) b; + outputDigest.update((byte) b); + System.out.print(c); + } + } + System.out.println(); + Assert.assertArrayEquals(streamDigest.digest(), outputDigest.digest()); + } +} diff --git a/src/test/java/org/oggio88/worth/serialization/json/JSONTest.java b/src/test/java/org/oggio88/worth/serialization/json/JSONTest.java new file mode 100644 index 0000000..0efd8cf --- /dev/null +++ b/src/test/java/org/oggio88/worth/serialization/json/JSONTest.java @@ -0,0 +1,134 @@ +package org.oggio88.worth.serialization.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; +import org.oggio88.worth.buffer.CircularBuffer; +import org.oggio88.worth.exception.NotImplementedException; +import org.oggio88.worth.utils.WorthUtils; +import org.oggio88.worth.value.ArrayValue; +import org.oggio88.worth.value.ObjectValue; +import org.oggio88.worth.xface.Parser; +import org.oggio88.worth.xface.Value; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.util.Map; + +public class JSONTest { + + private String[] testFiles = new String[]{"/test.json", "/wordpress.json"}; + + private InputStream getTestSource(String filename) { + return getClass().getResourceAsStream(filename); + } + + private boolean compareValueAndJsonNode(Value value, JsonNode jsonNode) { + switch (value.type()) { + case NULL: + return jsonNode.getNodeType() == JsonNodeType.NULL; + case INTEGER: + if (jsonNode.getNodeType() == JsonNodeType.NUMBER) { + return value.asInteger() == jsonNode.asLong(); + } else { + return false; + } + case DOUBLE: + if (jsonNode.getNodeType() == JsonNodeType.NUMBER) { + return value.asFloat() == jsonNode.asDouble(); + } else { + return false; + } + case BOOLEAN: + if (jsonNode.getNodeType() == JsonNodeType.BOOLEAN) { + return value.asBoolean() == jsonNode.asBoolean(); + } else { + return false; + } + case STRING: + if (jsonNode.getNodeType() == JsonNodeType.STRING) { + return value.asString().equals(jsonNode.asText()); + } else { + return false; + } + case ARRAY: + ArrayValue array = WorthUtils.dynamicCast(value, ArrayValue.class); + if (jsonNode.getNodeType() == JsonNodeType.ARRAY && array.size() == jsonNode.size()) { + for (int i = 0; i < array.size(); i++) { + if (!compareValueAndJsonNode(array.get(i), jsonNode.get(i))) { + return false; + } + } + return true; + } else { + return false; + } + case OBJECT: + ObjectValue object = WorthUtils.dynamicCast(value, ObjectValue.class); + if (jsonNode.getNodeType() == JsonNodeType.OBJECT) { + for (Map.Entry entry : object) { + if (!jsonNode.has(entry.getKey())) { + return false; + } else if (!compareValueAndJsonNode(entry.getValue(), jsonNode.get(entry.getKey()))) + return false; + } + return true; + } else { + return false; + } + default: + throw new NotImplementedException("This should never happen"); + } + } + + @Test + @SneakyThrows + public void consistencyTest() { + System.setProperty("org.oggio88.javason.value.ObjectValue.preserveKeyOrder", "true"); + for (String testFile : testFiles) { + Parser parser = new JSONParser(); + Value parsedValue = parser.parse(getTestSource(testFile)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JSONDumper.newInstance().dump(parsedValue, baos); + String dumpedJSON = new String(baos.toByteArray()); + byte[] barray = baos.toByteArray(); + ByteArrayInputStream bais = new ByteArrayInputStream(barray); + parser = new JSONParser(); + Value reParsedValue = parser.parse(bais); + Assert.assertEquals(parsedValue, reParsedValue); + baos = new ByteArrayOutputStream(); + JSONDumper.newInstance().dump(reParsedValue, baos); + String reDumpedJSON = new String(baos.toByteArray()); + Assert.assertEquals(dumpedJSON, reDumpedJSON); + } + } + + @Test + @SneakyThrows + public void comparativeTest() { + for (String testFile : testFiles) { + ObjectMapper om = new ObjectMapper(); + JsonNode jsonNode = om.readTree(getTestSource(testFile)); + Value value = new JSONParser().parse(getTestSource(testFile)); + Assert.assertTrue(compareValueAndJsonNode(value, jsonNode)); + } + } + + @Test + @SneakyThrows + public void hexTest() { + String hex = "1F608"; + byte[] buffer = new String(hex).getBytes(); + ByteArrayInputStream bais = new ByteArrayInputStream(buffer); + Method method = JSONParser.class.getDeclaredMethod("parseHex", CircularBuffer.class); + method.setAccessible(true); + int result = (int) method.invoke(null, new CircularBuffer(new InputStreamReader(bais), 5)); + Assert.assertEquals((int) Integer.valueOf(hex, 16), result); + } +} diff --git a/src/test/java/org/oggio88/worth/serialization/json/PerformanceTest.java b/src/test/java/org/oggio88/worth/serialization/json/PerformanceTest.java new file mode 100644 index 0000000..089201c --- /dev/null +++ b/src/test/java/org/oggio88/worth/serialization/json/PerformanceTest.java @@ -0,0 +1,111 @@ +package org.oggio88.worth.serialization.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.Ignore; +import org.junit.Test; +import org.oggio88.worth.xface.Value; +import org.tukaani.xz.XZInputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Paths; + +class Chronometer { + + public enum TimeUnit { + NANOSECOND(1e-9), MICROSECOND(1e-6), MILLISECOND(1e-3), SECOND(1); + + private double factor; + + TimeUnit(double factor) { + this.factor = factor; + } + } + + private long start = System.nanoTime(); + + public void start() { + start = System.nanoTime(); + } + + public void reset() { + start(); + } + + public double stop(TimeUnit unit) { + return (System.nanoTime() - start) / (1e9 * unit.factor); + } + + public double stop() { + return stop(TimeUnit.MILLISECOND); + } + +} + +public class PerformanceTest { + + @SneakyThrows + private static byte[] extractTestData() { + ByteArrayOutputStream baous = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024 * 1024]; + try (InputStream is = new XZInputStream(PerformanceTest.class.getResourceAsStream("/citylots.json.xz"))) { + while (true) { + int read = is.read(buffer); + if (read < 0) break; + baous.write(buffer, 0, read); + } + } + return baous.toByteArray(); + } + + @Test + @SneakyThrows + public void loopTest() { + double jacksonTime, worthTime; + final int loops = 100; + Chronometer chr = new Chronometer(); + { + chr.reset(); + for (int i = 0; i < loops; i++) { + ObjectMapper om = new ObjectMapper(); + JsonNode jsonNode = om.readTree(getClass().getResourceAsStream("/wordpress.json")); + } + jacksonTime = chr.stop(Chronometer.TimeUnit.MILLISECOND); + System.out.printf("Jackson time: %8s msec\n", String.format("%.3f", jacksonTime)); + } + { + chr.reset(); + for (int i = 0; i < loops; i++) { + Value value = new JSONParser().parse(getClass().getResourceAsStream("/wordpress.json")); + } + worthTime = chr.stop(Chronometer.TimeUnit.MILLISECOND); + System.out.printf("Worth time: %8s msec\n", String.format("%.3f", worthTime)); + } + } + + @Test + @Ignore + @SneakyThrows + public void hugeJSONTest() { + byte[] testData = extractTestData(); + double jacksonTime, worthTime; + Chronometer chr = new Chronometer(); + { + chr.reset(); + ObjectMapper om = new ObjectMapper(); + JsonNode jsonNode = om.readTree(new ByteArrayInputStream(testData)); + jacksonTime = chr.stop(Chronometer.TimeUnit.SECOND); + System.out.printf("Jackson time: %8s sec\n", String.format("%.3f", jacksonTime)); + } + { + chr.reset(); + Value value = new JSONParser().parse(new ByteArrayInputStream(testData)); + worthTime = chr.stop(Chronometer.TimeUnit.SECOND); + System.out.printf("Worth time: %8s sec\n", String.format("%.3f", worthTime)); + } + } +} diff --git a/src/test/resources/JSON.g4 b/src/test/resources/JSON.g4 new file mode 100644 index 0000000..b15a1f7 --- /dev/null +++ b/src/test/resources/JSON.g4 @@ -0,0 +1,80 @@ + +/** Taken from "The Definitive ANTLR 4 Reference" by Terence Parr */ + +// Derived from http://json.org +grammar JSON; + +json + : value + ; + +obj + : '{' pair (',' pair)* '}' + | '{' '}' + ; + +pair + : STRING ':' value + ; + +array + : '[' value (',' value)* ']' + | '[' ']' + ; + +value + : STRING + | NUMBER + | obj + | array + | 'true' + | 'false' + | 'null' + ; + + +STRING + : '"' (ESC | SAFECODEPOINT)* '"' + ; + + +fragment ESC + : '\\' (["\\/bfnrt] | UNICODE) + ; + + +fragment UNICODE + : 'u' HEX HEX HEX HEX + ; + + +fragment HEX + : [0-9a-fA-F] + ; + + +fragment SAFECODEPOINT + : ~ ["\\\u0000-\u001F] + ; + + +NUMBER + : '-'? INT ('.' [0-9] +)? EXP? + ; + + +fragment INT + : '0' | [1-9] [0-9]* + ; + +// no leading zeros + +fragment EXP + : [Ee] [+\-]? INT + ; + +// \- since - means "range" inside [...] + +WS + : [ \t\n\r] + -> skip + ; diff --git a/src/test/resources/citylots.json.xz b/src/test/resources/citylots.json.xz new file mode 100644 index 0000000..e791d7c Binary files /dev/null and b/src/test/resources/citylots.json.xz differ diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties new file mode 100644 index 0000000..64541a1 --- /dev/null +++ b/src/test/resources/logging.properties @@ -0,0 +1,36 @@ +handlers=java.util.logging.ConsoleHandler +config= +sun.net.www.protocol.http.HttpURLConnection.handlers=java.util.logging.ConsoleHandler +#sun.net.www.protocol.http.HttpURLConnection.level=ALL + +java.util.logging.FileHandler.level = WARNING +java.util.logging.FileHandler.filter = +java.util.logging.FileHandler.formatter = +java.util.logging.FileHandler.encoding = +java.util.logging.FileHandler.limit = +java.util.logging.FileHandler.count = +java.util.logging.FileHandler.append = false +java.util.logging.FileHandler.pattern = log.%u.%g.txt + +#java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.filter = +java.util.logging.ConsoleHandler.formatter = +java.util.logging.ConsoleHandler.encoding = + +java.util.logging.StreamHandler.level = WARNING +java.util.logging.StreamHandler.filter = +java.util.logging.StreamHandler.formatter = +java.util.logging.StreamHandler.encoding = + +java.util.logging.SocketHandler.level = WARNING +java.util.logging.SocketHandler.filter = +java.util.logging.SocketHandler.formatter = +java.util.logging.SocketHandler.encoding = +java.util.logging.SocketHandler.host = +java.util.logging.SocketHandler.port = + +java.util.logging.MemoryHandler.level = WARNING +java.util.logging.MemoryHandler.filter = +java.util.logging.MemoryHandler.size = +java.util.logging.MemoryHandler.push = +java.util.logging.MemoryHandler.target = \ No newline at end of file diff --git a/src/test/resources/test.json b/src/test/resources/test.json new file mode 100644 index 0000000..79787ff --- /dev/null +++ b/src/test/resources/test.json @@ -0,0 +1,31 @@ +{ + "widget": { + "debug": "on", + "window": { + "parent" : null, + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "name": "sun1", + "hOffset": 250, + "vOffset": 250, + "alignment": "center", + "tags" : ["Ireland", "Amazon", "development"], + "monochromatic" : false + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "name": "text1", + "hOffset": 250, + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} diff --git a/src/test/resources/wordpress.json b/src/test/resources/wordpress.json new file mode 100644 index 0000000..44a89ad --- /dev/null +++ b/src/test/resources/wordpress.json @@ -0,0 +1,318 @@ +[ + { + "id": 168697, + "date": "2018-09-06T09:30:53", + "date_gmt": "2018-09-06T16:30:53", + "guid": { + "rendered": "https://www.sitepoint.com/?p=168697" + }, + "modified": "2018-09-04T21:31:02", + "modified_gmt": "2018-09-05T04:31:02", + "slug": "the-8-best-wordpress-themes-for-small-business-websites", + "status": "publish", + "type": "post", + "link": "https://www.sitepoint.com/the-8-best-wordpress-themes-for-small-business-websites/", + "title": { + "rendered": "The 8 Best WordPress Themes for Small Business Websites" + }, + "content": { + "rendered": "

This article was created in partnership with BAWMedia. Thank you for supporting the partners who make SitePoint possible.

\n

A well-designed website can serve as a powerful marketing tool. These days, creating one for a small business is not distressing or expensive at all. However, it was just a few short years ago.

\n

Today, you can take advantage of the features provided by the best WordPress themes. There are special themes for small business-oriented websites. It's not difficult to find a website-building theme that matches a specific business. This can be a startup, a service provider, or some other venture.

\n

You undoubtedly want nothing but the best business theme, right? Check out those described below. Each possesses functional designs loaded with amazing features. They will help you create a thoroughly engaging website to promote a business.

\n

1. Be Theme

\n

\"\"

\n

We’ll start with Be Theme, a responsive, multipurpose WordPress theme that takes every small business need into account with its more than 370 pre-built websites. There's a multiplicity of small business WordPress themes in this pre-built website collection \u2014 each one embedded with the functionality you need to establish an effective online presence, and fully customizable to meet your business and marketing needs.

\n

The range of business niches covered is impressive, With more pre-built websites being added every month it's destined to become even more so. Web designers like Be Theme because it allows them to create a website for most small business types in as little as four hours.

\n

Clients appreciate the rapid turnaround they receive and the ease in which changes or additions they have in mind can be accommodated.

\n

Be Theme, one of the best WordPress themes for small business websites is a ThemeForest top 5 best seller whose core features include easy to work with page-building tools, a multiplicity of design features and options, and great support.

\n

2. Astra

\n

\"\"

\n

Astra is fast, fully customizable, and one of the best WordPress themes for business websites as well as for blogs and personal portfolios. Built with SEO in mind, Astra is responsive and WooCommerce ready \u2014 mandatory features in today's online business environment. Its capabilities are easily extendible with premium addons and Astra can be used with most of the popular page builders. This free WP-based theme is definitely worth considering.

\n

3. The100

\n

\"\"

\n

A theme selected for WordPress for small businesses can be free or it can be a premium theme requiring an expenditure on your behalf. There are several excellent free themes on the market, and one of them is The100. While it is advertised as having premium-like features, bear in mind that free themes like this one generally can't compete with premium themes. Nevertheless, The100 is an easy-to-use WP theme that features a multiplicity of layouts and plenty of customization options.

\n

4. Uncode \u2013 Creative Multiuse WordPress Theme

\n

\"\"

\n

Uncode has proven to be one of the best WordPress themes for business websites. It's a multipurpose theme featuring 30+ homepage concepts designed to get designers and their clients off to a fast start on any small business website. Features include an enhanced version of the popular Visual Composer page builder, and an Adaptive Images System that enables mobile users to see what you want and expect them to see.

\n

5. Houzez \u2013 Highly Customizable Real Estate WordPress Theme

\n

\"\"

\n

Some WordPress themes are created with a specific purpose in mind. Houzez is a specialty theme offering the features and functionality realtors and real estate agencies look for to promote their businesses and their marketability. Houzez' features include advanced property search filters, IDX systems, property management functionality, and solid customer support.

\n

6. TheGem \u2013 Creative Multi-Purpose High-Performance WordPress Theme

\n

\"\"

\n

TheGem is without doubt one of the best WordPress business themes on the market. Its users like working with the trendy design concepts the authors have presented based on their analysis of current UX trends. Visual Composer is TheGems' page builder, and a judiciously selected set of plugins gives the web designer the flexibility to satisfy any small business's needs. The package includes a ready-to-go online fashion store.

\n

7. Cesis \u2014 Responsive Multi-Purpose WordPress Theme

\n

\"\"

\n

When you're searching among the best WordPress themes for small business websites, Cesis is definitely worth a closer look. Its easy-to-use interface combined with a host of design elements and options allows you to build virtually anything you want. This is an important attribute when working with small businesses and startups, each having their unique business model and branding style.

\n

8. Pofo – Creative Portfolio and Blog WordPress Theme

\n

\"\"

\n

Web designers in need of small business WordPress themes include those whose clients represent creative teams and agencies as well as individual artists. Pofo is an ideal choice with its portfolio, eCommerce and blog features, bundled plugins, and more than 150 pre-built design elements. This premium theme's package also includes a nice assortment of home pages and more than 200 demo pages. Pofo is fully responsive, visually stunning, highly flexible, SEO and loading speed optimized.

\n

Conclusion

\n

Did you like this selection of the best WordPress themes for small business websites? It provides you with a wide range of options and merits close and careful study.

\n

You really can't make a bad choice. With a little extra effort, you should be able to walk away with a perfect WordPress theme. It can be ideal for creating a certain small business website you have in mind. Likewise, it can help you create a range of websites for small businesses.

\n", + "protected": false + }, + "excerpt": { + "rendered": "

This article was created in partnership with BAWMedia. Thank you for supporting the partners who make SitePoint possible.

\n

A well-designed website can serve as a powerful marketing tool. These days, creating one for a small business is not distressing or expensive at all. However, it was just a few short years ago.

\n

Today, you can take advantage of the features provided by the best WordPress themes. There are special themes for small business-oriented websites. It's not difficult to find a website-building theme that matches a specific business. This can be a startup, a service provider, or some other venture.

\n

You undoubtedly want nothing but the best business theme, right? Check out those described below. Each possesses functional designs loaded with amazing features. They will help you create a thoroughly engaging website to promote a business.

\n

1. Be Theme

\n

\"\"

\n

We’ll start with Be Theme, a responsive, multipurpose WordPress theme that takes every small business need into account with its more than 370 pre-built websites. There's a multiplicity of small business WordPress themes in this pre-built website collection \u2014 each one embedded with the functionality you need to establish an effective online presence, and fully customizable to meet your business and marketing needs.

\n

The range of business niches covered is impressive, With more pre-built websites being added every month it's destined to become even more so. Web designers like Be Theme because it allows them to create a website for most small business types in as little as four hours.

\n

Clients appreciate the rapid turnaround they receive and the ease in which changes or additions they have in mind can be accommodated.

\n

Be Theme, one of the best WordPress themes for small business websites is a ThemeForest top 5 best seller whose core features include easy to work with page-building tools, a multiplicity of design features and options, and great support.

\n

2. Astra

\n

\"\"

\n

Astra is fast, fully customizable, and one of the best WordPress themes for business websites as well as for blogs and personal portfolios. Built with SEO in mind, Astra is responsive and WooCommerce ready \u2014 mandatory features in today's online business environment. Its capabilities are easily extendible with premium addons and Astra can be used with most of the popular page builders. This free WP-based theme is definitely worth considering.

\n

3. The100

\n

\"\"

\n

A theme selected for WordPress for small businesses can be free or it can be a premium theme requiring an expenditure on your behalf. There are several excellent free themes on the market, and one of them is The100. While it is advertised as having premium-like features, bear in mind that free themes like this one generally can't compete with premium themes. Nevertheless, The100 is an easy-to-use WP theme that features a multiplicity of layouts and plenty of customization options.

\n

4. Uncode \u2013 Creative Multiuse WordPress Theme

\n

\"\"

\n

Uncode has proven to be one of the best WordPress themes for business websites. It's a multipurpose theme featuring 30+ homepage concepts designed to get designers and their clients off to a fast start on any small business website. Features include an enhanced version of the popular Visual Composer page builder, and an Adaptive Images System that enables mobile users to see what you want and expect them to see.

\n", + "protected": false + }, + "author": 72596, + "featured_media": 168703, + "comment_status": "open", + "ping_status": "closed", + "sticky": false, + "template": "", + "format": "standard", + "meta": [], + "categories": [ + 5856, + 5849 + ], + "tags": [ + 11664, + 9553, + 6298 + ], + "_links": { + "self": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts/168697" + } + ], + "collection": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts" + } + ], + "about": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/types/post" + } + ], + "author": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/users/72596" + } + ], + "replies": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/comments?post=168697" + } + ], + "version-history": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts/168697/revisions" + } + ], + "wp:featuredmedia": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/media/168703" + } + ], + "wp:attachment": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/media?parent=168697" + } + ], + "wp:term": [ + { + "taxonomy": "category", + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/categories?post=168697" + }, + { + "taxonomy": "post_tag", + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/tags?post=168697" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } + }, + { + "id": 168397, + "date": "2018-09-06T09:30:31", + "date_gmt": "2018-09-06T16:30:31", + "guid": { + "rendered": "https://www.sitepoint.com/?p=168397" + }, + "modified": "2018-09-04T00:20:42", + "modified_gmt": "2018-09-04T07:20:42", + "slug": "integrating-mongodb-and-amazon-kinesis-for-intelligent-durable-streams", + "status": "publish", + "type": "post", + "link": "https://www.sitepoint.com/integrating-mongodb-and-amazon-kinesis-for-intelligent-durable-streams/", + "title": { + "rendered": "Integrating MongoDB and Amazon Kinesis for Intelligent, Durable Streams" + }, + "content": { + "rendered": "

This article was originally published on MongoDB. Thank you for supporting the partners who make SitePoint possible.

\n

You can build your online, operational workloads atop MongoDB and still respond to events in real time by kicking off Amazon Kinesis stream processing actions, using MongoDB Stitch Triggers.

\n

Let\u2019s look at an example scenario in which a stream of data is being generated as a result of actions users take on a website. We\u2019ll durably store the data and simultaneously feed a Kinesis process to do streaming analytics on something like cart abandonment, product recommendations, or even credit card fraud detection.

\n

We\u2019ll do this by setting up a Stitch Trigger. When relevant data updates are made in MongoDB, the trigger will use a Stitch Function to call out to AWS Kinesis, as you can see in this architecture diagram:

\n

\"\"

\n

What you\u2019ll need to follow along

\n
    \n
  1. An Atlas instance
    \nIf you don\u2019t already have an application running on Atlas, you can follow our getting started with Atlas guide here. In this example, we\u2019ll be using a database called streamdata, with a collection called clickdata where we\u2019re writing data from our web-based e-commerce application.
  2. \n
  3. An AWS account and a Kinesis stream
    \nIn this example, we\u2019ll use a Kinesis stream to send data downstream to additional applications such as Kinesis Analytics. This is the stream we want to feed our updates into.
  4. \n
  5. A Stitch application
    \nIf you don\u2019t already have a Stitch application, log into Atlas, and click Stitch Apps from the navigation on the left, then click Create New Application.
  6. \n
\n

Create a Collection

\n

The first step is to create a database and collection from the Stitch application console. Click Rules from the left navigation menu and click the Add Collection button. Type streamdata for the database and clickdata for the collection name. Select the template labeled Users can only read and write their own data and provide a field name where we\u2019ll specify the user id.

\n

\"Figure

\n

Configuring Stitch to Talk to AWS

\n

Stitch lets you configure Services to interact with external services such as AWS Kinesis. Choose Services from the navigation on the left, and click the Add a Service button, select the AWS service and set AWS Access Key ID, and Secret Access Key.

\n

\"Figure

\n

Services use Rules to specify what aspect of a service Stitch can use, and how. Add a rule which will enable that service to communicate with Kinesis by clicking the button labeled NEW RULE. Name the rule \u201ckinesis\u201d as we\u2019ll be using this specific rule to enable communication with AWS Kinesis. In the section marked Action, select the API labeled Kinesis and select All Actions.

\n

\"Figure

\n

Write a Function that Streams Documents into Kinesis

\n

Now that we have a working AWS service, we can use it to put records into a Kinesis stream. The way we do that in Stitch is with Functions. Let\u2019s set up a putKinesisRecord function.

\n

Select Functions from the left-hand menu, and click Create New Function. Provide a name for the function and paste the following in the body of the function.

\n

\"Figure

\n
exports = function(event){\r\n const awsService = context.services.get('aws');\r\ntry{\r\n   awsService.kinesis().PutRecord({\r\n     Data: JSON.stringify(event.fullDocument),\r\n     StreamName: \"stitchStream\",\r\n     PartitionKey: \"1\"\r\n      }).then(function(response) {\r\n        return response;\r\n      });\r\n}\r\ncatch(error){\r\n  console.log(JSON.parse(error));\r\n}\r\n};\r\n
\n

Test Out the Function

\n

Let\u2019s make sure everything is working by calling that function manually. From the Function Editor, Click Console to view the interactive javascript console for Stitch.

\n

\"\"

\n

Functions called from Triggers require an event. To test execution of our function, we\u2019ll need to pass a dummy event to the function. Creating variables from the console in Stitch is simple. Simply set the value of the variable to a JSON document. For our simple example, use the following:

\n
event = {\r\n   \"operationType\": \"replace\",\r\n   \"fullDocument\": {\r\n       \"color\": \"black\",\r\n       \"inventory\": {\r\n           \"$numberInt\": \"1\"\r\n       },\r\n       \"overview\": \"test document\",\r\n       \"price\": {\r\n           \"$numberDecimal\": \"123\"\r\n       },\r\n       \"type\": \"backpack\"\r\n   },\r\n   \"ns\": {\r\n       \"db\": \"streamdata\",\r\n       \"coll\": \"clickdata\"\r\n   }\r\n}\r\nexports(event);\r\n
\n

Paste the above into the console and click the button labeled Run Function As. Select a user and the function will execute.

\n

Ta-da!

\n

Putting It Together with Stitch Triggers

\n

We\u2019ve got our MongoDB collection living in Atlas, receiving events from our web app. We\u2019ve got our Kinesis stream ready for data. We\u2019ve got a Stitch Function that can put data into a Kinesis stream.

\n

Configuring Stitch Triggers is so simple it\u2019s almost anticlimactic. Click Triggers from the left navigation, name your trigger, provide the database and collection context, and select the database events Stitch will react to with execution of a function.

\n

For the database and collection, use the names from step one. Now we\u2019ll set the operations we want to watch with our trigger. (Some triggers might care about all of them \u2013 inserts, updates, deletes, and replacements \u2013 while others can be more efficient because they logically can only matter for some of those.) In our case, we\u2019re going to watch for insert, update and replace operations.

\n

Now we specify our putKinesisRecord function as the linked function, and we\u2019re done.

\n

\"Figure

\n

As part of trigger execution, Stitch will forward details associated with the trigger event, including the full document involved in the event (i.e. the newly inserted, updated, or deleted document from the collection.) This is where we can evaluate some condition or attribute of the incoming document and decide whether or not to put the record onto a stream.

\n

Test the Trigger!

\n

Amazon provides a dashboard which will enable you to view details associated with the data coming into your stream.

\n

\"Figure

\n

As you execute the function from within Stitch, you\u2019ll begin to see the data entering the Kinesis stream.

\n

Building More Functionality

\n

So far our trigger is pretty basic \u2013 it watches a collection and when any updates or inserts happen, it feeds the entire document to our Kinesis stream. From here we can build out some more intelligent functionality. To wrap up this post, let\u2019s look at what we can do with the data once it\u2019s been durably stored in MongoDB and placed into a stream.

\n

Once the record is in the Kinesis Stream you can configure additional services downstream to act on the data. A common use case incorporates Amazon Kinesis Data Analytics to perform analytics on the streaming data. Amazon Kinesis Data Analytics offers pre-configured templates to accomplish things like anomaly detection, simple alerts, aggregations, and more.

\n

For example, our stream of data will contain orders resulting from purchases. These orders may originate from point-of-sale systems, as well as from our web-based e-commerce application. Kinesis Analytics can be leveraged to create applications that process the incoming stream of data. For our example, we could build a machine learning algorithm to detect anomalies in the data or create a product performance leaderboard from a sliding, or tumbling window of data from our stream.

\n

\"Figure

\n

Wrapping Up

\n

Now you can connect MongoDB to Kinesis. From here, you\u2019re able to leverage any one of the many services offered from Amazon Web Services to build on your application. In our next article in the series, we\u2019ll focus on getting the data back from Kinesis into MongoDB. In the meantime, let us know what you\u2019re building with Atlas, Stitch, and Kinesis!

\n

Resources

\n

MongoDB Atlas

\n\n

MongoDB Stitch

\n\n

Amazon Kinesis

\n\n", + "protected": false + }, + "excerpt": { + "rendered": "

This article was originally published on MongoDB. Thank you for supporting the partners who make SitePoint possible.

\n

You can build your online, operational workloads atop MongoDB and still respond to events in real time by kicking off Amazon Kinesis stream processing actions, using MongoDB Stitch Triggers.

\n

Let\u2019s look at an example scenario in which a stream of data is being generated as a result of actions users take on a website. We\u2019ll durably store the data and simultaneously feed a Kinesis process to do streaming analytics on something like cart abandonment, product recommendations, or even credit card fraud detection.

\n

We\u2019ll do this by setting up a Stitch Trigger. When relevant data updates are made in MongoDB, the trigger will use a Stitch Function to call out to AWS Kinesis, as you can see in this architecture diagram:

\n

\"\"

\n

What you\u2019ll need to follow along

\n
    \n
  1. An Atlas instance
    \nIf you don\u2019t already have an application running on Atlas, you can follow our getting started with Atlas guide here. In this example, we\u2019ll be using a database called streamdata, with a collection called clickdata where we\u2019re writing data from our web-based e-commerce application.
  2. \n
  3. An AWS account and a Kinesis stream
    \nIn this example, we\u2019ll use a Kinesis stream to send data downstream to additional applications such as Kinesis Analytics. This is the stream we want to feed our updates into.
  4. \n
  5. A Stitch application
    \nIf you don\u2019t already have a Stitch application, log into Atlas, and click Stitch Apps from the navigation on the left, then click Create New Application.
  6. \n
\n

Create a Collection

\n

The first step is to create a database and collection from the Stitch application console. Click Rules from the left navigation menu and click the Add Collection button. Type streamdata for the database and clickdata for the collection name. Select the template labeled Users can only read and write their own data and provide a field name where we\u2019ll specify the user id.

\n

\"Figure

\n

Configuring Stitch to Talk to AWS

\n

Stitch lets you configure Services to interact with external services such as AWS Kinesis. Choose Services from the navigation on the left, and click the Add a Service button, select the AWS service and set AWS Access Key ID, and Secret Access Key.

\n

\"Figure

\n

Services use Rules to specify what aspect of a service Stitch can use, and how. Add a rule which will enable that service to communicate with Kinesis by clicking the button labeled NEW RULE. Name the rule \u201ckinesis\u201d as we\u2019ll be using this specific rule to enable communication with AWS Kinesis. In the section marked Action, select the API labeled Kinesis and select All Actions.

\n

\"Figure

\n

Write a Function that Streams Documents into Kinesis

\n

Now that we have a working AWS service, we can use it to put records into a Kinesis stream. The way we do that in Stitch is with Functions. Let\u2019s set up a putKinesisRecord function.

\n

Select Functions from the left-hand menu, and click Create New Function. Provide a name for the function and paste the following in the body of the function.

\n

\"Figure

\n
exports = function(event){\r\n const awsService = context.services.get('aws');\r\ntry{\r\n   awsService.kinesis().PutRecord({\r\n     Data: JSON.stringify(event.fullDocument),\r\n     StreamName: \"stitchStream\",\r\n     PartitionKey: \"1\"\r\n      }).then(function(response) {\r\n        return response;\r\n      });\r\n}\r\ncatch(error){\r\n  console.log(JSON.parse(error));\r\n}\r\n};\r\n
\n

Test Out the Function

\n

Let\u2019s make sure everything is working by calling that function manually. From the Function Editor, Click Console to view the interactive javascript console for Stitch.

\n", + "protected": false + }, + "author": 72676, + "featured_media": 168458, + "comment_status": "open", + "ping_status": "closed", + "sticky": false, + "template": "", + "format": "standard", + "meta": [], + "categories": [ + 422 + ], + "tags": [ + 9553, + 1599, + 6298 + ], + "_links": { + "self": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts/168397" + } + ], + "collection": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts" + } + ], + "about": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/types/post" + } + ], + "author": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/users/72676" + } + ], + "replies": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/comments?post=168397" + } + ], + "version-history": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts/168397/revisions" + } + ], + "wp:featuredmedia": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/media/168458" + } + ], + "wp:attachment": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/media?parent=168397" + } + ], + "wp:term": [ + { + "taxonomy": "category", + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/categories?post=168397" + }, + { + "taxonomy": "post_tag", + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/tags?post=168397" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } + }, + { + "id": 167869, + "date": "2018-09-05T09:30:47", + "date_gmt": "2018-09-05T16:30:47", + "guid": { + "rendered": "https://www.sitepoint.com/?p=167869" + }, + "modified": "2018-09-06T01:21:52", + "modified_gmt": "2018-09-06T08:21:52", + "slug": "how-to-set-up-a-secure-relational-database-on-alibaba-cloud", + "status": "publish", + "type": "post", + "link": "https://www.sitepoint.com/how-to-set-up-a-secure-relational-database-on-alibaba-cloud/", + "title": { + "rendered": "How to Set up a Secure Relational Database on Alibaba Cloud" + }, + "content": { + "rendered": "

\n

This article was originally published on Alibaba Cloud. Thank you for supporting the partners who make SitePoint possible.

\n

Think you got a better tip for making the best use of Alibaba Cloud services? Tell us about it and go in for your chance to win a Macbook Pro (plus other cool stuff). Find out more here.

\n

Gain an introduction to ApsaraDB for RDS, a cloud-based relational database product provided by Alibaba Cloud. In this webinar you will watch over the shoulder of a Solution Architect and Trainer, as he covers the basic concepts and features of ApsaraDB for RDS including:

\n
    \n
  • HA feature (Master/Slave Architecture, Backup/Recovery, Temporary Instance)
  • \n
  • Scalability features (Read-only Instance)
  • \n
  • Security and Monitoring features
  • \n
\n

This webinar is ideally suited for database engineers and beginners to the Alibaba Cloud product suite.

\n", + "protected": false + }, + "excerpt": { + "rendered": "

\n

This article was originally published on Alibaba Cloud. Thank you for supporting the partners who make SitePoint possible.

\n

Gain an introduction to ApsaraDB for RDS, a cloud-based relational database product provided by Alibaba Cloud. In this webinar you will watch over the shoulder of a Solution Architect and Trainer, as he covers the basic concepts and features of ApsaraDB for RDS including:

\n
    \n
  • HA feature (Master/Slave Architecture, Backup/Recovery, Temporary Instance)
  • \n
  • Scalability features (Read-only Instance)
  • \n
  • Security and Monitoring features
  • \n
\n

This webinar is ideally suited for database engineers and beginners to the Alibaba Cloud product suite.

\n", + "protected": false + }, + "author": 72596, + "featured_media": 167879, + "comment_status": "open", + "ping_status": "closed", + "sticky": false, + "template": "", + "format": "standard", + "meta": [], + "categories": [ + 1292, + 422 + ], + "tags": [ + 11731, + 9553 + ], + "_links": { + "self": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts/167869" + } + ], + "collection": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts" + } + ], + "about": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/types/post" + } + ], + "author": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/users/72596" + } + ], + "replies": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/comments?post=167869" + } + ], + "version-history": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/posts/167869/revisions" + } + ], + "wp:featuredmedia": [ + { + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/media/167879" + } + ], + "wp:attachment": [ + { + "href": "https://www.sitepoint.com/wp-json/wp/v2/media?parent=167869" + } + ], + "wp:term": [ + { + "taxonomy": "category", + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/categories?post=167869" + }, + { + "taxonomy": "post_tag", + "embeddable": true, + "href": "https://www.sitepoint.com/wp-json/wp/v2/tags?post=167869" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } + } +]