/*
* Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.errai.jpa.client.local.backend;
import java.util.ArrayList;
import java.util.List;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.jpa.client.local.EntityJsonMatcher;
import org.jboss.errai.jpa.client.local.ErraiEntityManager;
import org.jboss.errai.jpa.client.local.ErraiIdentifiableType;
import org.jboss.errai.jpa.client.local.ErraiManagedType;
import org.jboss.errai.jpa.client.local.JsonUtil;
import org.jboss.errai.jpa.client.local.Key;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
/**
* The storage backend for HTML WebStorage, a storage facility supported by most
* browsers for at least 2.5 million characters of data, (5 megabytes of Unicode
* text).
* <p>
* This backend supports <i>namespacing</i>, which is a way of dividing up the
* storage into any number of non-overlapping buckets. For any two namespaces
* <i>A</i> and <i>B</i> (<i>A</i> != <i>B</i>), the storage backend for
* namespace <i>A</i> will never see, modify, or otherwise or interfere with
* anything stored in the storage backend for namespace <i>B</i>.
*
* @author Jonathan Fuerth <jfuerth@gmail.com>
*/
public class WebStorageBackend implements StorageBackend {
public static final StorageBackendFactory FACTORY = new StorageBackendFactory() {
@Override
public StorageBackend createInstanceFor(ErraiEntityManager em) {
return new WebStorageBackend(em);
}
};
private final ErraiEntityManager em;
private final String namespace;
private final Logger logger;
/**
* Creates a WebStorageBackend that works with entities in the default storage
* namespace.
*
* @param erraiEntityManager
* the ErraiEntityManager this storage backend will be used with (it
* is used for resolving entity references).
*/
public WebStorageBackend(ErraiEntityManager erraiEntityManager) {
this(erraiEntityManager, "");
}
/**
* Creates a WebStorageBackend that works with entities in the given storage
* namespace.
*
* @param erraiEntityManager
* the ErraiEntityManager this storage backend will be used with (it
* is used for resolving entity references).
* @param namespace
* The namespace to operate within. Must not be null.
*/
public WebStorageBackend(ErraiEntityManager erraiEntityManager, String namespace) {
em = Assert.notNull(erraiEntityManager);
this.namespace = Assert.notNull(namespace);
this.logger = LoggerFactory.getLogger(WebStorageBackend.class);
}
@Override
public void removeAll() {
// this is done in two phases because it would be bad to modify the key set while iterating over it
final List<String> toRemove = new ArrayList<String>();
LocalStorage.forEachKey(new EntryVisitor() {
@Override
public void visit(String key, String value) {
if (parseNamespacedKey(em, key, false) != null) {
toRemove.add(key);
}
}
});
for (String key : toRemove) {
LocalStorage.remove(key);
}
}
@Override
public <X> void put(Key<X,?> key, X value) {
ErraiManagedType<X> entityType = key.getEntityType();
String keyJson = namespace + key.toJson();
JSONValue valueJson = entityType.toJson(em, value);
logger.trace(">>>put '" + keyJson + "'");
LocalStorage.put(keyJson, valueJson.toString());
}
@Override
public <X> X get(Key<X, ?> requestedKey) {
for (ErraiManagedType<? extends X> entityType : requestedKey.getEntityType().getSubtypes()) {
Key<X, ?> key = new Key<X, Object>((ErraiManagedType<X>) entityType, (Object) requestedKey.getId());
String keyJson = namespace + key.toJson();
String valueJson = LocalStorage.get(keyJson);
logger.trace("<<<get '" + keyJson + "' : " + valueJson);
X entity;
if (valueJson != null) {
entity = entityType.fromJson(em, JSONParser.parseStrict(valueJson));
logger.trace(" returning " + entity);
return entity;
}
}
return null;
}
@Override
public <X> List<X> getAll(final ErraiIdentifiableType<X> type, final EntityJsonMatcher matcher) {
// TODO index entries by entity type
final List<X> entities = new ArrayList<X>();
LocalStorage.forEachKey(new EntryVisitor() {
@Override
public void visit(String key, String value) {
Key<?, ?> k = parseNamespacedKey(em, key, false);
if (k == null) return;
logger.trace("getAll(): considering " + value);
if (type.isSuperclassOf(k.getEntityType())) {
logger.trace(" --> correct type");
JSONObject candidate = JSONParser.parseStrict(value).isObject();
Assert.notNull(candidate);
if (matcher.matches(candidate)) {
@SuppressWarnings("unchecked")
Key<X, ?> typedKey = (Key<X, ?>) k;
// Unfortunately, this throws away a lot of work we've already done (getting the entity type,
// creating the key, doing a backend.get(), parsing the JSON value, ...)
// it would be nice to avoid this, but we have to go back to the entity manager in case the
// thing we want is in the persistence context.
entities.add((X) em.find(k.getEntityType().getJavaType(), typedKey.getId()));
}
else {
logger.trace(" --> but not a match");
}
}
else {
logger.trace(" --> wrong type");
}
}
});
return entities;
}
@Override
public <X, Y> boolean contains(Key<X, Y> key) {
boolean contains = false;
for (ErraiManagedType<X> type : key.getEntityType().getSubtypes()) {
Key<?, ?> k = new Key<X, Y>(type, key.getId());
String keyJson = namespace + k.toJson();
contains = LocalStorage.get(keyJson) != null;
logger.trace("<<<contains '" + keyJson + "' : " + contains);
if (contains) break;
}
return contains;
}
@Override
public <X> void remove(Key<X, ?> key) {
String keyJson = namespace + key.toJson();
LocalStorage.remove(keyJson);
}
@Override
public <X> boolean isModified(Key<X, ?> key, X value) {
ErraiManagedType<X> entityType = key.getEntityType();
String keyJson = namespace + key.toJson();
JSONValue newValueJson = entityType.toJson(em, value);
JSONValue oldValueJson = JSONParser.parseStrict(LocalStorage.get(keyJson));
boolean modified = !JsonUtil.equals(newValueJson, oldValueJson);
if (modified) {
logger.trace("Detected modified entity " + key);
logger.trace(" Old: " + oldValueJson);
logger.trace(" New: " + newValueJson);
}
return modified;
}
private Key<?, ?> parseNamespacedKey(ErraiEntityManager em, String key, boolean failIfNotFound) {
if ( (!key.startsWith(namespace)) || namespace.length() >= key.length()) return null;
key = key.substring(namespace.length());
if (key.charAt(0) != '{') return null;
return Key.fromJson(em, key, failIfNotFound);
}
}