package org.infinispan.rest;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.Writer;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.Variant;
import org.codehaus.jackson.map.ObjectMapper;
import org.infinispan.AdvancedCache;
import org.infinispan.CacheSet;
import org.infinispan.commons.CacheException;
import org.infinispan.commons.hash.MurmurHash3;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.container.entries.CacheEntry;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.metadata.Metadata;
import org.infinispan.rest.configuration.RestServerConfiguration;
import org.jboss.resteasy.util.HttpHeaderNames;
import com.thoughtworks.xstream.XStream;
/**
* Integration server linking REST requests with Infinispan calls.
*
* @author Michael Neale
* @author Galder ZamarreƱo
* @since 4.0
*/
@Path("/")
public class Server {
public static final String READERS_ROLE = "_infinispan_rest_readers";
public static final String WRITERS_ROLE = "_infinispan_rest_writers";
private final RestServerConfiguration configuration;
private final RestCacheManager manager;
private final MurmurHash3 hashFunc = MurmurHash3.getInstance();
private final static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneId.systemDefault());
private final static MediaType TextPlainUtf8Type = new MediaType("text", "plain", "UTF-8");
private final static String TextPlainUtf8 = TextPlainUtf8Type.toString();
private final static MediaType ApplicationXJavaSerializedObjectType = new MediaType("application", "x-java-serialized-object");
private final static String ApplicationXJavaSerializedObject = ApplicationXJavaSerializedObjectType.toString();
private final static String TimeToLiveHeader = "timeToLiveSeconds";
private final static String MaxIdleTimeHeader = "maxIdleTimeSeconds";
/**
* For dealing with binary entries in the cache
*/
private static class VariantListHelper {
public static final List<Variant> variantList = Variant.VariantListBuilder.newInstance().mediaTypes(
MediaType.APPLICATION_XML_TYPE, ApplicationXJavaSerializedObjectType, MediaType.APPLICATION_JSON_TYPE).build();
}
private static class CollectionVariantListHelper {
public static final List<Variant> collectionVariantList = Variant.VariantListBuilder.newInstance().mediaTypes(
MediaType.TEXT_HTML_TYPE,
MediaType.APPLICATION_XML_TYPE,
MediaType.APPLICATION_JSON_TYPE,
MediaType.TEXT_PLAIN_TYPE,
TextPlainUtf8Type
).build();
}
private static class JsonMapperHolder {
public static final ObjectMapper jsonMapper = new ObjectMapper();
}
private static class XStreamholder {
public static final XStream XStream = new XStream();
}
public Server(RestServerConfiguration configuration, RestCacheManager manager) {
this.configuration = configuration;
this.manager = manager;
}
@GET
@Path("/{cacheName}")
@RolesAllowed({READERS_ROLE, WRITERS_ROLE})
public Response getKeys(@Context Request request, @HeaderParam("performAsync") boolean useAsync,
@PathParam("cacheName") String cacheName, @QueryParam("global") String globalKeySet) {
return protectCacheNotFound(() -> {
AdvancedCache<String, ?> cache = manager.getCache(cacheName);
CacheSet<String> keys = cache.keySet();
Variant variant = request.selectVariant(CollectionVariantListHelper.collectionVariantList);
String selectedMediaType = variant != null ? variant.getMediaType().toString() : null;
if (MediaType.TEXT_HTML.equals(selectedMediaType)) {
return Response.ok().type(MediaType.TEXT_HTML).entity(printIt(pw -> {
pw.print("<html><body>");
keys.forEach(key -> {
String hkey = Escaper.escapeHtml(key);
pw.printf("<a href=\"%s/%s\">%s</a><br/>", cacheName, hkey, hkey);
});
pw.print("</body></html>");
})).build();
} else if (MediaType.APPLICATION_XML.equals(selectedMediaType)) {
return Response.ok().type(MediaType.APPLICATION_XML).entity(printIt(pw -> {
pw.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + System.lineSeparator() + System.lineSeparator() + "<keys>");
keys.forEach(key -> pw.printf("<key>%s</key>", Escaper.escapeXml(key)));
pw.print("</keys>");
})).build();
} else if (MediaType.APPLICATION_JSON.equals(selectedMediaType)) {
return Response.ok().type(MediaType.APPLICATION_JSON).entity(printIt(pw -> {
pw.print("keys=[");
Iterator<String> it = keys.iterator();
while (it.hasNext()) {
pw.printf("\"%s\"", Escaper.escapeJson(it.next()));
if (it.hasNext()) pw.print(",");
}
pw.print("]");
})).build();
} else if (MediaType.TEXT_PLAIN.equals(selectedMediaType)) {
return Response.ok().type(MediaType.TEXT_PLAIN).entity(printIt(pw -> keys.forEach(pw::println))).build();
} else if (Server.TextPlainUtf8.equals(selectedMediaType)) {
return Response.ok().type(Server.TextPlainUtf8Type).entity(printItUTF8(writer -> {
keys.forEach(key -> {
try {
writer.write(key);
writer.write(System.lineSeparator());
} catch (IOException e) {
throw new CacheException(e);
}
});
})).build();
} else {
return Response.notAcceptable(CollectionVariantListHelper.collectionVariantList).build();
}
});
}
@GET
@Path("/{cacheName}/{cacheKey}")
@RolesAllowed({READERS_ROLE, WRITERS_ROLE})
public <V> Response getEntry(@Context Request request, @HeaderParam("performAsync") boolean useAsync,
@PathParam("cacheName") String cacheName, @PathParam("cacheKey") String key,
@QueryParam("extended") String extended,
@DefaultValue("") @HeaderParam("Cache-Control") String cacheControl) {
return protectCacheNotFound(() -> {
CacheEntry<String, V> entry = manager.getInternalEntry(cacheName, key);
if (entry instanceof InternalCacheEntry) {
InternalCacheEntry<String, V> ice = (InternalCacheEntry<String, V>) entry;
Date lastMod = lastModified(ice);
Date expires = ice.canExpire() ? new Date(ice.getExpiryTime()) : null;
OptionalInt minFreshSeconds = minFresh(cacheControl);
return ensureFreshEnoughEntry(expires, minFreshSeconds, () -> {
Metadata meta = ice.getMetadata();
if (meta instanceof MimeMetadata) {
return getMimeEntry(request, ice, (MimeMetadata) meta, lastMod, expires, cacheName, extended);
} else {
return getAnyEntry(request, ice, meta, lastMod, expires, cacheName, extended);
}
});
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
});
}
private Response ensureFreshEnoughEntry(Date expires, OptionalInt minFreshSeconds, Supplier<Response> supplier) {
if (entryFreshEnough(expires, minFreshSeconds)) {
return supplier.get();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
private OptionalInt minFresh(String cacheControl) {
Optional<String> minFreshDirective = Arrays.stream(cacheControl.split(",")).filter(s -> s.contains("min-fresh")).findFirst();
return minFreshDirective.map(s -> {
String[] equals = s.split("=");
return OptionalInt.of(Integer.parseInt(equals[equals.length - 1].trim()));
}).orElse(OptionalInt.empty());
}
private boolean entryFreshEnough(Date entryExpires, OptionalInt minFresh) {
return !minFresh.isPresent() || minFresh.getAsInt() < calcFreshness(entryExpires);
}
private int calcFreshness(Date expires) {
if (expires == null) {
return Integer.MAX_VALUE;
} else {
return ((int) (expires.getTime() - new Date().getTime()) / 1000);
}
}
private <V> Response getMimeEntry(Request request, InternalCacheEntry<String, V> ice, MimeMetadata meta,
Date lastMod, Date expires, String cacheName, String extended) {
String key = ice.getKey();
Response.ResponseBuilder bldr = request.evaluatePreconditions(lastMod, calcETAG(ice, meta));
if (bldr == null) {
bldr = extended(mortality(Response.ok(ice.getValue(), meta.contentType)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
//workaround for https://issues.jboss.org/browse/RESTEASY-887
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
meta).tag(calcETAG(ice, meta)),
cacheName, key, wantExtendedHeaders(extended));
}
return bldr.build();
}
private <V> Response getAnyEntry(Request request, InternalCacheEntry<String, V> ice, Metadata meta,
Date lastMod, Date expires, String cacheName, String extended) {
String key = ice.getKey();
V value = ice.getValue();
if (value instanceof String) {
return mortality(Response.ok((String) value, MediaType.TEXT_PLAIN)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.cacheControl(calcCacheControl(expires))
.header(HttpHeaderNames.EXPIRES, formatDate(expires)),
meta).build();
} else if (value instanceof byte[]) {
return extended(mortality(Response.ok()
.type(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
meta), cacheName, key, wantExtendedHeaders(extended))
.entity(streamIt(b -> {
try {
b.write((byte[]) value);
} catch (IOException e) {
throw new CacheException(e);
}
}))
.build();
} else {
Variant variant = request.selectVariant(VariantListHelper.variantList);
String selectedMediaType = variant != null ? variant.getMediaType().toString() : null;
// For objects other than String or byte arrays, accept only JSON, XML and X_JAVA_SERIALIZABLE_OBJECT
if (MediaType.APPLICATION_JSON.equals(selectedMediaType)) {
return extended(mortality(Response.ok()
.type(selectedMediaType)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
meta), cacheName, key, wantExtendedHeaders(extended))
.entity(streamIt(b -> {
try {
JsonMapperHolder.jsonMapper.writeValue(b, value);
} catch (IOException e) {
throw new CacheException(e);
}
}))
.build();
} else if (MediaType.APPLICATION_XML.equals(selectedMediaType)) {
return extended(mortality(Response.ok()
.type(selectedMediaType)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
meta), cacheName, key, wantExtendedHeaders(extended))
.entity(streamIt(b -> XStreamholder.XStream.toXML(value, b)))
.build();
} else if (Server.ApplicationXJavaSerializedObject.equals(selectedMediaType)) {
if (value instanceof Serializable) {
return extended(mortality(Response.ok()
.type(selectedMediaType)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
meta), cacheName, key, wantExtendedHeaders(extended))
.entity(streamIt(b -> {
try {
new ObjectOutputStream(b).writeObject(value);
} catch (IOException e) {
throw new CacheException(e);
}
}))
.build();
}
}
return Response.notAcceptable(VariantListHelper.variantList).build();
}
}
private String formatDate(Date date) {
if (date == null)
return null;
else
return DATE_TIME_FORMATTER.format(date.toInstant());
}
private CacheControl calcCacheControl(Date expires) {
if (expires == null) {
return null;
} else {
CacheControl cacheControl = new CacheControl();
int maxAgeSeconds = calcFreshness(expires);
if (maxAgeSeconds > 0)
cacheControl.setMaxAge(maxAgeSeconds);
else
cacheControl.setNoCache(true);
return cacheControl;
}
}
static Response.ResponseBuilder mortality(Response.ResponseBuilder bld, Metadata meta) {
if (meta.lifespan() > -1)
bld.header(Server.TimeToLiveHeader, TimeUnit.MILLISECONDS.toSeconds(meta.lifespan()));
if (meta.maxIdle() > -1)
bld.header(Server.MaxIdleTimeHeader, TimeUnit.MILLISECONDS.toSeconds(meta.maxIdle()));
return bld;
}
Response.ResponseBuilder extended(Response.ResponseBuilder bld, String cacheName, String key, boolean b) {
return b ? bld
.header("Cluster-Primary-Owner", manager.getPrimaryOwner(cacheName, key))
.header("Cluster-Node-Name", manager.getNodeName())
.header("Cluster-Server-Address", manager.getServerAddress()) : bld;
}
private boolean wantExtendedHeaders(String extended) {
switch (configuration.extendedHeaders()) {
case NEVER:
return false;
case ON_DEMAND:
return extended != null;
default:
throw new IllegalArgumentException("Unsupported header:" + configuration.extendedHeaders());
}
}
/**
* create a JAX-RS streaming output
*/
StreamingOutput streamIt(Consumer<? super OutputStream> action) {
return new StreamingOutput() {
@Override
public void write(OutputStream o) {
action.accept(o);
}
};
}
StreamingOutput printIt(Consumer<? super PrintWriter> consumer) {
return new StreamingOutput() {
@Override
public void write(OutputStream o) throws IOException, WebApplicationException {
PrintWriter pw = new PrintWriter(o);
try {
consumer.accept(pw);
} finally {
pw.flush();
}
}
};
}
StreamingOutput printItUTF8(Consumer<? super Writer> action) {
return new StreamingOutput() {
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
Writer writer = new OutputStreamWriter(outputStream, "UTF-8");
try {
action.accept(writer);
} finally {
writer.flush();
}
}
};
}
@HEAD
@Path("/{cacheName}/{cacheKey}")
@RolesAllowed({READERS_ROLE, WRITERS_ROLE})
public <V> Response headEntry(@Context Request request, @HeaderParam("performAsync") boolean useAsync,
@PathParam("cacheName") String cacheName, @PathParam("cacheKey") String key,
@QueryParam("extended") String extended,
@DefaultValue("") @HeaderParam("Cache-Control") String cacheControl) {
return protectCacheNotFound(() -> {
CacheEntry<String, V> entry = manager.getInternalEntry(cacheName, key);
if (entry instanceof InternalCacheEntry) {
InternalCacheEntry<String, V> ice = (InternalCacheEntry<String, V>) entry;
Date lastMod = lastModified(ice);
Date expires = ice.canExpire() ? new Date(ice.getExpiryTime()) : null;
OptionalInt minFreshSeconds = minFresh(cacheControl);
return ensureFreshEnoughEntry(expires, minFreshSeconds, () -> {
Metadata meta = ice.getMetadata();
if (meta instanceof MimeMetadata) {
MimeMetadata mime = (MimeMetadata) meta;
Response.ResponseBuilder bldr = request.evaluatePreconditions(lastMod, calcETAG(ice, mime));
if (bldr == null) {
return extended(mortality(Response.ok()
.type(mime.contentType)
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
mime)
.tag(calcETAG(ice, mime)),
cacheName, key, wantExtendedHeaders(extended))
.build();
} else {
return bldr.build();
}
} else {
return extended(mortality(Response.ok()
.header(HttpHeaderNames.LAST_MODIFIED, formatDate(lastMod))
.header(HttpHeaderNames.EXPIRES, formatDate(expires))
.cacheControl(calcCacheControl(expires)),
meta), cacheName, key, wantExtendedHeaders(extended))
.build();
}
});
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
});
}
@PUT
@POST
@Path("/{cacheName}/{cacheKey}")
@RolesAllowed(WRITERS_ROLE)
public <V> Response putEntry(@Context Request request, @HeaderParam("performAsync") boolean useAsync,
@PathParam("cacheName") String cacheName, @PathParam("cacheKey") String key,
@HeaderParam("Content-Type") String mediaType, byte[] data,
@DefaultValue("-1") @HeaderParam("timeToLiveSeconds") long ttl,
@DefaultValue("-1") @HeaderParam("maxIdleTimeSeconds") long idleTime) {
return protectCacheNotFound(() -> {
AdvancedCache<String, byte[]> cache = manager.getCache(cacheName);
if ("POST".equals(request.getMethod()) && cache.containsKey(key)) {
return Response.status(Response.Status.CONFLICT).build();
} else {
CacheEntry<String, V> entry = manager.getInternalEntry(cacheName, key, true);
if (entry instanceof InternalCacheEntry) {
InternalCacheEntry ice = (InternalCacheEntry) entry;
Date lastMod = lastModified(ice);
Metadata meta = ice.getMetadata();
if (meta instanceof MimeMetadata) {
// The item already exists in the cache, evaluate preconditions based on its attributes and the headers
EntityTag etag = calcETAG(ice, (MimeMetadata) meta);
Response.ResponseBuilder bldr = request.evaluatePreconditions(lastMod, etag);
if (bldr == null) {
// Preconditions passed
return putInCache(useAsync, cache, key, data, mediaType, ttl, idleTime,
Optional.of((byte[]) ice.getValue()));
} else {
// One of the preconditions failed, build a response
return bldr.build();
}
} else {
return putInCache(useAsync, cache, key, data, mediaType, ttl, idleTime, Optional.empty());
}
} else {
return putInCache(useAsync, cache, key, data, mediaType, ttl, idleTime, Optional.empty());
}
}
});
}
private Response putInCache(boolean useAsync, AdvancedCache<String, byte[]> cache, String key,
byte[] data, String dataType, long ttl, long idleTime, Optional<byte[]> prevCond) {
if (useAsync)
return asyncPutInCache(cache, key, data, dataType, ttl, idleTime);
else
return putOrReplace(cache, key, data, dataType, ttl, idleTime, prevCond);
}
Response asyncPutInCache(AdvancedCache<String, byte[]> cache,
String key, byte[] data, String dataType, long ttl, long idleTime) {
Metadata metadata = createMetadata(cache.getCacheConfiguration(), dataType, ttl, idleTime);
cache.putAsync(key, data, metadata);
return Response.ok().build();
}
Metadata createMetadata(Configuration cfg, String dataType, long ttl, long idleTime) {
MimeMetadataBuilder metadata = new MimeMetadataBuilder();
metadata.contentType(dataType);
if (ttl == 0) {
metadata.lifespan(cfg.expiration().lifespan(), TimeUnit.MILLISECONDS);
} else {
metadata.lifespan(ttl, TimeUnit.SECONDS);
}
if (idleTime == 0) {
metadata.maxIdle(cfg.expiration().maxIdle(), TimeUnit.MILLISECONDS);
} else {
metadata.maxIdle(idleTime, TimeUnit.SECONDS);
}
return metadata.build();
}
private Response putOrReplace(AdvancedCache<String, byte[]> cache,
String key, byte[] data, String dataType,
long ttl, long idleTime,
Optional<byte[]> prevCond) {
Metadata metadata = createMetadata(cache.getCacheConfiguration(), dataType, ttl, idleTime);
if (prevCond.isPresent()) {
boolean replaced = cache.replace(key, prevCond.get(), data, metadata);
// If not replaced, simply send back that the precondition failed
if (replaced) {
return Response.ok().build();
} else {
return Response.status(HttpServletResponse.SC_PRECONDITION_FAILED).build();
}
} else {
cache.put(key, data, metadata);
return Response.ok().build();
}
}
@DELETE
@Path("/{cacheName}/{cacheKey}")
@RolesAllowed(WRITERS_ROLE)
public <V> Response removeEntry(@Context Request request, @HeaderParam("performAsync") boolean useAsync,
@PathParam("cacheName") String cacheName, @PathParam("cacheKey") String key) {
return protectCacheNotFound(() -> {
CacheEntry<String, V> entry = manager.getInternalEntry(cacheName, key);
if (entry instanceof InternalCacheEntry) {
InternalCacheEntry ice = (InternalCacheEntry) entry;
Date lastMod = lastModified((InternalCacheEntry) entry);
Metadata meta = entry.getMetadata();
if (meta instanceof MimeMetadata) {
// The item exists in the cache, evaluate preconditions based on its attributes and the headers
EntityTag etag = calcETAG(ice, (MimeMetadata) meta);
Response.ResponseBuilder bldr = request.evaluatePreconditions(lastMod, etag);
if (bldr == null) {
// Preconditions passed
if (useAsync) {
manager.getCache(cacheName).removeAsync(key);
} else {
manager.getCache(cacheName).remove(key);
}
return Response.ok().build();
} else {
// One of the preconditions failed, build a response
return bldr.build();
}
} else {
if (useAsync) {
manager.getCache(cacheName).removeAsync(key);
} else {
manager.getCache(cacheName).remove(key);
}
return Response.ok().build();
}
} else if (entry == null) {
return Response.status(Response.Status.NOT_FOUND).build();
} else {
throw new IllegalArgumentException("Unsupported entry implementation: " + entry);
}
});
}
@DELETE
@Path("/{cacheName}")
@RolesAllowed(WRITERS_ROLE)
public Response killCache(@PathParam("cacheName") String cacheName,
@DefaultValue("") @HeaderParam("If-Match") String ifMatch,
@DefaultValue("") @HeaderParam("If-None-Match") String ifNoneMatch,
@DefaultValue("") @HeaderParam("If-Modified-Since") String ifModifiedSince,
@DefaultValue("") @HeaderParam("If-Unmodified-Since") String ifUnmodifiedSince) {
if (ifMatch.isEmpty() && ifNoneMatch.isEmpty() && ifModifiedSince.isEmpty() && ifUnmodifiedSince.isEmpty()) {
manager.getCache(cacheName).clear();
return Response.ok().build();
} else {
return preconditionNotImplementedResponse();
}
}
private Response preconditionNotImplementedResponse() {
return Response.status(501).entity(
"Preconditions were not implemented yet for PUT, POST, and DELETE methods.").build();
}
private <K, V> EntityTag calcETAG(InternalCacheEntry<K, V> entry, MimeMetadata meta) {
return new EntityTag(meta.contentType + hashFunc.hash(entry.getValue()));
}
private <K, V> Date lastModified(InternalCacheEntry<K, V> ice) {
return new Date(ice.getCreated() / 1000 * 1000);
}
private Response protectCacheNotFound(Supplier<Response> op) {
try {
return op.get();
} catch (CacheNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}
class CacheNotFoundException extends CacheException {
public CacheNotFoundException(String msg) {
super(msg);
}
}
class CacheUnavailableException extends CacheException {
public CacheUnavailableException(String msg) {
super(msg);
}
}
class Escaper {
static String escapeHtml(String html) {
return escapeXml(html);
}
static String escapeXml(String xml) {
StringBuilder sb = new StringBuilder();
for (char c : xml.toCharArray()) {
switch (c) {
case '&':
sb.append("&");
break;
case '>':
sb.append(">");
break;
case '<':
sb.append("<");
break;
case '\"':
sb.append(""");
break;
case '\'':
sb.append("'");
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
}
static String escapeJson(String json) {
return json.replaceAll("\"", "\\\\\"");
}
}