blob: 3dff1a6f18290e0f0961f0e9d70e74675e8f8410 [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you 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.apache.sling.api.scripting;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.script.Bindings;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ConsumerType;
/**
* <p>
* The {@code LazyBindings} wraps another map and dynamically provides entries for the wrapped map through a map of {@link
* LazyBindings.Supplier}s.
* </p>
* <p>
* When {@link #get(Object)} is called with a {@code key} that's not present in the wrapped map, then the {@link LazyBindings.Supplier}s map
* will be queried and, if an entry exists for that key, the {@link LazyBindings.Supplier}-generated value will be used to populate the
* wrapped map.
* </p>
* <p>
* While the {@link #keySet()} and {@link #containsKey(Object)} will also check the keys present in the {@link LazyBindings.Supplier}s map,
* all other methods (e.g. {@link #values()}, {@link #containsValue(Object)}) will only deal with the contents of the wrapped map.
* <p>
* {@link #entrySet()} will however return a merged view of both the {@link LazyBindings.Supplier}s and the wrapped map, so that copies to
* other {@code LazyBindings} maps preserve the functionality of having lazily-evaluated bindings.</p>
* <p>
* This class <b>does not provide any thread-safety guarantees</b>. If {@code this} {@code Bindings} map needs to be used in a concurrent
* setup it's the responsibility of the caller to synchronize access. The simplest way would be to wrap it through {@link
* Collections#synchronizedMap(Map)}.
* </p>
*/
@ConsumerType
public class LazyBindings extends HashMap<String, Object> implements Bindings {
private final Map<String, LazyBindings.Supplier> suppliers;
public LazyBindings() {
this(new HashMap<>(), Collections.emptyMap());
}
public LazyBindings(Map<String, LazyBindings.Supplier> suppliers) {
this(suppliers, Collections.emptyMap());
}
public LazyBindings(Map<String, LazyBindings.Supplier> suppliers, Map<String, Object> wrapped) {
super(wrapped);
this.suppliers = suppliers;
}
@Override
public Object put(String key, Object value) {
Object previous = super.get(key);
if (value instanceof LazyBindings.Supplier) {
suppliers.put(key, (LazyBindings.Supplier) value);
} else {
super.put(key, value);
}
return previous;
}
@Override
public void putAll(Map<? extends String, ?> toMerge) {
for (Entry<? extends String, ?> entry : toMerge.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
super.clear();
suppliers.clear();
}
@NotNull
@Override
public Set<String> keySet() {
Set<String> keySet = new HashSet<>(super.keySet());
if (!suppliers.isEmpty()) {
keySet.addAll(suppliers.keySet());
}
return Collections.unmodifiableSet(keySet);
}
@NotNull
@Override
public Collection<Object> values() {
return super.values();
}
@NotNull
@Override
public Set<Entry<String, Object>> entrySet() {
HashSet<Entry<String, Object>> entrySet = new HashSet<>(super.entrySet());
for (Map.Entry supplierEntry : suppliers.entrySet()) {
entrySet.add(supplierEntry);
}
return Collections.unmodifiableSet(entrySet);
}
@Override
public int size() {
Set<String> keys = new HashSet<>(super.keySet());
keys.addAll(suppliers.keySet());
return keys.size();
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public boolean containsKey(Object key) {
return super.containsKey(key) || suppliers.containsKey(key);
}
@Override
public Object get(Object key) {
String k = key.toString();
if (!super.containsKey(k) && suppliers.containsKey(k)) {
Object value = suppliers.get(k).get();
super.put(k, value);
suppliers.remove(k);
}
return super.get(key);
}
@Override
public Object remove(Object key) {
Object previous = super.remove(key);
if (previous == null) {
LazyBindings.Supplier supplier = suppliers.remove(key);
if (supplier != null) {
return supplier.get();
}
}
return previous;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof LazyBindings && super.equals(o)) {
LazyBindings other = (LazyBindings) o;
return suppliers.equals(other.suppliers);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode() + suppliers.hashCode();
}
@Override
public Object getOrDefault(Object key, Object defaultValue) {
Object result = get(key);
if (result == null) {
return defaultValue;
}
return result;
}
/**
* This marker interface should be used for suppliers which should be unwrapped when used as values stored in a {@link LazyBindings} map.
*/
@ConsumerType
@FunctionalInterface
public interface Supplier extends java.util.function.Supplier {}
}