blob: 8b6c3a8b81923ffef93761166db340242fe5b0c0 [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.juneau.cp;
import static org.apache.juneau.collections.JsonMap.*;
import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static java.util.stream.Collectors.*;
import java.lang.annotation.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.marshaller.*;
import org.apache.juneau.reflect.*;
/**
* Java bean store.
*
* <p>
* A simple storage database for beans keyed by type and name.
* Used to retrieve and instantiate beans using an injection-like API.
* It's similar in concept to the injection framework of Spring but greatly simplified in function and not intended to implement a full-fledged injection framework.
*
* <p>
* Beans can be stored with or without names. Named beans are typically resolved using
* the <ja>@Named</ja> or <ja>@Qualified</ja> annotations on constructor or method parameters.
*
* <p>
* Beans are added through the following methods:
* <ul class='javatreec'>
* <li class='jm'>{@link #add(Class,Object) add(Class,Object)}
* <li class='jm'>{@link #add(Class,Object,String) add(Class,Object,String)}
* <li class='jm'>{@link #addBean(Class,Object) addBean(Class,Object)}
* <li class='jm'>{@link #addBean(Class,Object,String) addBean(Class,Object,String)}
* <li class='jm'>{@link #addSupplier(Class,Supplier) addSupplier(Class,Supplier)}
* <li class='jm'>{@link #addSupplier(Class,Supplier,String) addSupplier(Class,Supplier,String)}
* </ul>
*
* <p>
* Beans are retrieved through the following methods:
* <ul class='javatreec'>
* <li class='jm'>{@link #getBean(Class) getBean(Class)}
* <li class='jm'>{@link #getBean(Class,String) getBean(Class,String)}
* <li class='jm'>{@link #stream(Class) stream(Class)}
* </ul>
*
* <p>
* Beans are created through the following methods:
* <ul class='javatreec'>
* <li class='jm'>{@link #createBean(Class) createBean(Class)}
* <li class='jm'>{@link #createMethodFinder(Class) createMethodFinder(Class)}
* <li class='jm'>{@link #createMethodFinder(Class,Class) createMethodFinder(Class,Class)}
* <li class='jm'>{@link #createMethodFinder(Class,Object) createMethodFinder(Class,Object)}
* </ul>
*
* <h5 class='section'>Notes:</h5><ul>
* <li class='note'>Bean stores can be nested using {@link Builder#parent(BeanStore)}.
* <li class='note'>Bean stores can be made read-only using {@link Builder#readOnly()}.
* <li class='note'>Bean stores can be made thread-safe using {@link Builder#threadSafe()}.
* </ul>
*
* <h5 class='section'>See Also:</h5><ul>
* </ul>
*/
public class BeanStore {
//-----------------------------------------------------------------------------------------------------------------
// Static
//-----------------------------------------------------------------------------------------------------------------
/**
* Non-existent bean store.
*/
public static final class Void extends BeanStore {}
/**
* Static read-only reusable instance.
*/
public static final BeanStore INSTANCE = create().readOnly().build();
/**
* Static creator.
*
* @return A new {@link Builder} object.
*/
public static Builder create() {
return new Builder();
}
/**
* Static creator.
*
* @param parent Parent bean store. Can be <jk>null</jk> if this is the root resource.
* @return A new {@link BeanStore} object.
*/
public static BeanStore of(BeanStore parent) {
return create().parent(parent).build();
}
/**
* Static creator.
*
* @param parent Parent bean store. Can be <jk>null</jk> if this is the root resource.
* @param outer The outer bean used when instantiating inner classes. Can be <jk>null</jk>.
* @return A new {@link BeanStore} object.
*/
public static BeanStore of(BeanStore parent, Object outer) {
return create().parent(parent).outer(outer).build();
}
//-----------------------------------------------------------------------------------------------------------------
// Builder
//-----------------------------------------------------------------------------------------------------------------
/**
* Builder class.
*/
@FluentSetters
public static class Builder {
BeanStore parent;
boolean readOnly, threadSafe;
Object outer;
Class<? extends BeanStore> type;
BeanStore impl;
/**
* Constructor.
*/
protected Builder() {}
/**
* Instantiates this bean store.
*
* @return A new bean store.
*/
public BeanStore build() {
if (impl != null)
return impl;
if (type == null || type == BeanStore.class)
return new BeanStore(this);
ClassInfo c = ClassInfo.of(type);
MethodInfo m = c.getDeclaredMethod(
x -> x.isPublic()
&& x.hasNoParams()
&& x.isStatic()
&& x.hasName("getInstance")
);
if (m != null)
return m.invoke(null);
ConstructorInfo ci = c.getPublicConstructor(x -> x.canAccept(this));
if (ci != null)
return ci.invoke(this);
ci = c.getDeclaredConstructor(x -> x.isProtected() && x.canAccept(this));
if (ci != null)
return ci.accessible().invoke(this);
throw new BasicRuntimeException("Could not find a way to instantiate class {0}", type);
}
//-------------------------------------------------------------------------------------------------------------
// Properties
//-------------------------------------------------------------------------------------------------------------
/**
* Specifies the parent bean store.
*
* <p>
* Bean searches are performed recursively up this parent chain.
*
* @param value The setting value.
* @return This object.
*/
@FluentSetter
public Builder parent(BeanStore value) {
parent = value;
return this;
}
/**
* Specifies that the bean store is read-only.
*
* <p>
* This means methods such as {@link BeanStore#addBean(Class, Object)} cannot be used.
*
* @return This object.
*/
@FluentSetter
public Builder readOnly() {
readOnly = true;
return this;
}
/**
* Specifies that the bean store being created should be thread-safe.
*
* @return This object.
*/
@FluentSetter
public Builder threadSafe() {
threadSafe = true;
return this;
}
/**
* Specifies the outer bean context.
*
* <p>
* The outer context bean to use when calling constructors on inner classes.
*
* @param value The outer bean context. Can be <jk>null</jk>.
* @return This object.
*/
@FluentSetter
public Builder outer(Object value) {
this.outer = value;
return this;
}
/**
* Overrides the bean to return from the {@link #build()} method.
*
* @param value The bean to return from the {@link #build()} method.
* @return This object.
*/
@FluentSetter
public Builder impl(BeanStore value) {
this.impl = value;
return this;
}
/**
* Overrides the bean store type.
*
* <p>
* The specified type must have one of the following:
* <ul>
* <li>A static <c>getInstance()</c> method.
* <li>A public constructor that takes in this builder.
* <li>A protected constructor that takes in this builder.
* </ul>
*
* @param value The bean store type.
* @return This object.
*/
@FluentSetter
public Builder type(Class<? extends BeanStore> value) {
this.type = value;
return this;
}
}
//-----------------------------------------------------------------------------------------------------------------
// Instance
//-----------------------------------------------------------------------------------------------------------------
private final Deque<BeanStoreEntry<?>> entries;
private final Map<Class<?>,BeanStoreEntry<?>> unnamedEntries;
final Optional<BeanStore> parent;
final Optional<Object> outer;
final boolean readOnly, threadSafe;
final SimpleReadWriteLock lock;
BeanStore() {
this(create());
}
/**
* Constructor.
*
* @param builder The builder containing the settings for this bean.
*/
protected BeanStore(Builder builder) {
parent = optional(builder.parent);
outer = optional(builder.outer);
readOnly = builder.readOnly;
threadSafe = builder.threadSafe;
lock = threadSafe ? new SimpleReadWriteLock() : SimpleReadWriteLock.NO_OP;
entries = threadSafe ? new ConcurrentLinkedDeque<>() : linkedList();
unnamedEntries = threadSafe ? new ConcurrentHashMap<>() : map();
}
/**
* Adds an unnamed bean of the specified type to this factory.
*
* @param <T> The class to associate this bean with.
* @param beanType The class to associate this bean with.
* @param bean The bean. Can be <jk>null</jk>.
* @return This object.
*/
public <T> BeanStore addBean(Class<T> beanType, T bean) {
return addBean(beanType, bean, null);
}
/**
* Adds a named bean of the specified type to this factory.
*
* @param <T> The class to associate this bean with.
* @param beanType The class to associate this bean with.
* @param bean The bean. Can be <jk>null</jk>.
* @param name The bean name if this is a named bean. Can be <jk>null</jk>.
* @return This object.
*/
public <T> BeanStore addBean(Class<T> beanType, T bean, String name) {
return addSupplier(beanType, ()->bean, name);
}
/**
* Adds a supplier for an unnamed bean of the specified type to this factory.
*
* @param <T> The class to associate this bean with.
* @param beanType The class to associate this bean with.
* @param bean The bean supplier.
* @return This object.
*/
public <T> BeanStore addSupplier(Class<T> beanType, Supplier<T> bean) {
return addSupplier(beanType, bean, null);
}
/**
* Adds a supplier for a named bean of the specified type to this factory.
*
* @param <T> The class to associate this bean with.
* @param beanType The class to associate this bean with.
* @param bean The bean supplier.
* @param name The bean name if this is a named bean. Can be <jk>null</jk>.
* @return This object.
*/
public <T> BeanStore addSupplier(Class<T> beanType, Supplier<T> bean, String name) {
assertCanWrite();
BeanStoreEntry<T> e = createEntry(beanType, bean, name);
try (SimpleLock x = lock.write()) {
entries.addFirst(e);
if (isEmpty(name))
unnamedEntries.put(beanType, e);
}
return this;
}
/**
* Same as {@link #addBean(Class,Object)} but returns the bean instead of this object for fluent calls.
*
* @param <T> The class to associate this bean with.
* @param beanType The class to associate this bean with.
* @param bean The bean. Can be <jk>null</jk>.
* @return The bean.
*/
public <T> T add(Class<T> beanType, T bean) {
add(beanType, bean, null);
return bean;
}
/**
* Same as {@link #addBean(Class,Object,String)} but returns the bean instead of this object for fluent calls.
*
* @param <T> The class to associate this bean with.
* @param beanType The class to associate this bean with.
* @param bean The bean. Can be <jk>null</jk>.
* @param name The bean name if this is a named bean. Can be <jk>null</jk>.
* @return The bean.
*/
public <T> T add(Class<T> beanType, T bean, String name) {
addBean(beanType, bean, name);
return bean;
}
/**
* Clears out all bean in this bean store.
*
* <p>
* Does not affect the parent bean store.
*
* @return This object.
*/
public BeanStore clear() {
assertCanWrite();
try (SimpleLock x = lock.write()) {
unnamedEntries.clear();
entries.clear();
}
return this;
}
/**
* Returns the unnamed bean of the specified type.
*
* @param <T> The type of bean to return.
* @param beanType The type of bean to return.
* @return The bean.
*/
@SuppressWarnings("unchecked")
public <T> Optional<T> getBean(Class<T> beanType) {
try (SimpleLock x = lock.read()) {
BeanStoreEntry<T> e = (BeanStoreEntry<T>) unnamedEntries.get(beanType);
if (e != null)
return optional(e.get());
if (parent.isPresent())
return parent.get().getBean(beanType);
return empty();
}
}
/**
* Returns the named bean of the specified type.
*
* @param <T> The type of bean to return.
* @param beanType The type of bean to return.
* @param name The bean name. Can be <jk>null</jk>.
* @return The bean.
*/
@SuppressWarnings("unchecked")
public <T> Optional<T> getBean(Class<T> beanType, String name) {
try (SimpleLock x = lock.read()) {
BeanStoreEntry<T> e = (BeanStoreEntry<T>)entries.stream().filter(x2 -> x2.matches(beanType, name)).findFirst().orElse(null);
if (e != null)
return optional(e.get());
if (parent.isPresent())
return parent.get().getBean(beanType, name);
return empty();
}
}
/**
* Returns all the beans in this store of the specified type.
*
* <p>
* Returns both named and unnamed beans.
*
* <p>
* The results from the parent bean store are appended to the list of beans from this beans store.
*
* @param <T> The bean type to return.
* @param beanType The bean type to return.
* @return The bean entries. Never <jk>null</jk>.
*/
public <T> Stream<BeanStoreEntry<T>> stream(Class<T> beanType) {
@SuppressWarnings("unchecked")
Stream<BeanStoreEntry<T>> s = entries.stream().filter(x -> x.matches(beanType)).map(x -> (BeanStoreEntry<T>)x);
if (parent.isPresent())
s = Stream.concat(s, parent.get().stream(beanType));
return s;
}
/**
* Removes an unnamed bean from this store.
*
* @param beanType The bean type being removed.
* @return This object.
*/
public BeanStore removeBean(Class<?> beanType) {
return removeBean(beanType, null);
}
/**
* Removes a named bean from this store.
*
* @param beanType The bean type being removed.
* @param name The bean name to remove.
* @return This object.
*/
public BeanStore removeBean(Class<?> beanType, String name) {
assertCanWrite();
try (SimpleLock x = lock.write()) {
if (name == null)
unnamedEntries.remove(beanType);
entries.removeIf(y -> y.matches(beanType, name));
}
return this;
}
/**
* Returns <jk>true</jk> if this store contains the specified unnamed bean type.
*
* @param beanType The bean type to check.
* @return <jk>true</jk> if this store contains the specified unnamed bean type.
*/
public boolean hasBean(Class<?> beanType) {
return unnamedEntries.containsKey(beanType) || parent.map(x -> x.hasBean(beanType)).orElse(false);
}
/**
* Returns <jk>true</jk> if this store contains the specified named bean type.
*
* @param beanType The bean type to check.
* @param name The bean name.
* @return <jk>true</jk> if this store contains the specified named bean type.
*/
public boolean hasBean(Class<?> beanType, String name) {
return entries.stream().anyMatch(x -> x.matches(beanType, name)) || parent.map(x -> x.hasBean(beanType, name)).orElse(false);
}
/**
* Instantiates a bean creator.
*
* <h5 class='section'>See Also:</h5><ul>
* <li class='jc'>{@link BeanCreator} for usage.
* </ul>
*
* @param <T> The bean type to create.
* @param beanType The bean type to create.
* @return A new bean creator.
*/
public <T> BeanCreator<T> createBean(Class<T> beanType) {
return new BeanCreator<>(beanType, this);
}
/**
* Create a method finder for finding bean creation methods.
*
* <h5 class='section'>See Also:</h5><ul>
* <li class='jc'>{@link BeanCreateMethodFinder} for usage.
* </ul>
*
* @param <T> The bean type to create.
* @param beanType The bean type to create.
* @param resource The class containing the bean creator method.
* @return The method finder. Never <jk>null</jk>.
*/
public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType, Object resource) {
return new BeanCreateMethodFinder<>(beanType, resource, this);
}
/**
* Create a method finder for finding bean creation methods.
*
* <p>
* Same as {@link #createMethodFinder(Class,Class)} but looks for only static methods on the specified resource class
* and not also instance methods within the context of a bean.
*
* <h5 class='section'>See Also:</h5><ul>
* <li class='jc'>{@link BeanCreateMethodFinder} for usage.
* </ul>
*
* @param <T> The bean type to create.
* @param beanType The bean type to create.
* @param resourceClass The class containing the bean creator method.
* @return The method finder. Never <jk>null</jk>.
*/
public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType, Class<?> resourceClass) {
return new BeanCreateMethodFinder<>(beanType, resourceClass , this);
}
/**
* Create a method finder for finding bean creation methods.
*
* <p>
* Same as {@link #createMethodFinder(Class,Object)} but uses {@link Builder#outer(Object)} as the resource bean.
*
* <h5 class='section'>See Also:</h5><ul>
* <li class='jc'>{@link BeanCreateMethodFinder} for usage.
* </ul>
*
* @param <T> The bean type to create.
* @param beanType The bean type to create.
* @return The method finder. Never <jk>null</jk>.
*/
public <T> BeanCreateMethodFinder<T> createMethodFinder(Class<T> beanType) {
return new BeanCreateMethodFinder<>(beanType, outer.orElseThrow(() -> new IllegalArgumentException("Method cannot be used without outer bean definition.")), this);
}
/**
* Given an executable, returns a list of types that are missing from this factory.
*
* @param executable The constructor or method to get the params for.
* @return A comma-delimited list of types that are missing from this factory, or <jk>null</jk> if none are missing.
*/
public String getMissingParams(ExecutableInfo executable) {
List<ParamInfo> params = executable.getParams();
List<String> l = list();
loop: for (int i = 0; i < params.size(); i++) {
ParamInfo pi = params.get(i);
ClassInfo pt = pi.getParameterType();
if (i == 0 && outer.isPresent() && pt.isInstance(outer.get()))
continue loop;
if (pt.is(Optional.class) || pt.is(BeanStore.class))
continue loop;
String beanName = findBeanName(pi);
Class<?> ptc = pt.inner();
if (beanName == null && !hasBean(ptc))
l.add(pt.getSimpleName());
if (beanName != null && !hasBean(ptc, beanName))
l.add(pt.getSimpleName() + '@' + beanName);
}
return l.isEmpty() ? null : l.stream().sorted().collect(joining(","));
}
/**
* Given the list of param types, returns <jk>true</jk> if this factory has all the parameters for the specified executable.
*
* @param executable The constructor or method to get the params for.
* @return A comma-delimited list of types that are missing from this factory.
*/
public boolean hasAllParams(ExecutableInfo executable) {
loop: for (int i = 0; i < executable.getParamCount(); i++) {
ParamInfo pi = executable.getParam(i);
ClassInfo pt = pi.getParameterType();
if (i == 0 && outer.isPresent() && pt.isInstance(outer.get()))
continue loop;
if (pt.is(Optional.class) || pt.is(BeanStore.class))
continue loop;
String beanName = findBeanName(pi);
Class<?> ptc = pt.inner();
if ((beanName == null && !hasBean(ptc)) || (beanName != null && !hasBean(ptc, beanName)))
return false;
}
return true;
}
/**
* Returns the corresponding beans in this factory for the specified param types.
*
* @param executable The constructor or method to get the params for.
* @return The corresponding beans in this factory for the specified param types.
*/
public Object[] getParams(ExecutableInfo executable) {
Object[] o = new Object[executable.getParamCount()];
for (int i = 0; i < executable.getParamCount(); i++) {
ParamInfo pi = executable.getParam(i);
ClassInfo pt = pi.getParameterType();
if (i == 0 && outer.isPresent() && pt.isInstance(outer.get())) {
o[i] = outer.get();
} else if (pt.is(BeanStore.class)) {
o[i] = this;
} else {
String beanName = findBeanName(pi);
Class<?> ptc = pt.unwrap(Optional.class).inner();
Optional<?> o2 = beanName == null ? getBean(ptc) : getBean(ptc, beanName);
o[i] = pt.is(Optional.class) ? o2 : o2.orElse(null);
}
}
return o;
}
@Override /* Object */
public String toString() {
return Json5.of(properties());
}
//-----------------------------------------------------------------------------------------------------------------
// Extension methods
//-----------------------------------------------------------------------------------------------------------------
/**
* Creates an entry in this store for the specified bean.
*
* <p>
* Subclasses can override this method to create their own entry subtypes.
*
* @param <T> The class type to associate with the bean.
* @param type The class type to associate with the bean.
* @param bean The bean supplier.
* @param name Optional name to associate with the bean. Can be <jk>null</jk>.
* @return A new bean store entry.
*/
protected <T> BeanStoreEntry<T> createEntry(Class<T> type, Supplier<T> bean, String name) {
return BeanStoreEntry.create(type, bean, name);
}
// <FluentSetters>
// </FluentSetters>
//-----------------------------------------------------------------------------------------------------------------
// Helper methods
//-----------------------------------------------------------------------------------------------------------------
private String findBeanName(ParamInfo pi) {
Annotation n = pi.getAnnotation(Annotation.class, x -> x.annotationType().getSimpleName().equals("Named"));
if (n != null)
return AnnotationInfo.of((ClassInfo)null, n).getValue(String.class, "value", NOT_EMPTY).orElse(null);
return null;
}
private void assertCanWrite() {
if (readOnly)
throw new IllegalStateException("Method cannot be used because BeanStore is read-only.");
}
private JsonMap properties() {
Predicate<Boolean> nf = ObjectUtils::isTrue;
return filteredMap()
.append("identity", ObjectUtils.identity(this))
.append("entries", entries.stream().map(BeanStoreEntry::properties).collect(toList()))
.append("outer", ObjectUtils.identity(outer.orElse(null)))
.append("parent", parent.map(BeanStore::properties).orElse(null))
.appendIf(nf, "readOnly", readOnly)
.appendIf(nf, "threadSafe", threadSafe)
;
}
}