LANG-609: Added AtomicInitializer class. Introduced new ConcurrentInitializer interface which is now implemented by all all initializer classes.
git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@929189 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
e4789bd4fc
commit
f96d4df26e
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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.commons.lang3.concurrent;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A specialized implementation of the {@code ConcurrentInitializer} interface
|
||||
* based on an {@link AtomicReference} variable.
|
||||
* </p>
|
||||
* <p>
|
||||
* This class maintains a member field of type {@code AtomicReference}. It
|
||||
* implements the following algorithm to create and initialize an object in its
|
||||
* {@link #get()} method:
|
||||
* <ul>
|
||||
* <li>First it is checked whether the {@code AtomicReference} variable contains
|
||||
* already a value. If this is the case, the value is directly returned.</li>
|
||||
* <li>Otherwise the {@link #initialize()} method is called. This method must be
|
||||
* defined in concrete subclasses to actually create the managed object.</li>
|
||||
* <li>After the object was created by {@link #initialize()} it is checked
|
||||
* whether the {@code AtomicReference} variable is still undefined. This has to
|
||||
* be done because in the meantime another thread may have initialized the
|
||||
* object. If the reference is still empty, the newly created object is stored
|
||||
* in it and returned by this method.</li>
|
||||
* <li>Otherwise the value stored in the {@code AtomicReference} is returned.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>
|
||||
* Because atomic variables are used this class does not need any
|
||||
* synchronization. So there is no danger of deadlock, and access to the managed
|
||||
* object is efficient. However, if multiple threads access the {@code
|
||||
* AtomicInitializer} object before it has been initialized almost at the same
|
||||
* time, it can happen that {@link #initialize()} is called multiple times. The
|
||||
* algorithm outlined above guarantees that {@link #get()} always returns the
|
||||
* same object though.
|
||||
* </p>
|
||||
* <p>
|
||||
* Compared with the {@link LazyInitializer} class, this class can be more
|
||||
* efficient because it does not need synchronization. The drawback is that the
|
||||
* {@link #initialize()} method can be called multiple times which may be
|
||||
* problematic if the creation of the managed object is expensive. As a rule of
|
||||
* thumb this initializer implementation is preferable if there are not too many
|
||||
* threads involved and the probability that multiple threads access an
|
||||
* uninitialized object is small. If there is high parallelism,
|
||||
* {@link LazyInitializer} is more appropriate.
|
||||
* </p>
|
||||
*
|
||||
* @author Apache Software Foundation
|
||||
* @version $Id$
|
||||
* @param <T> the type of the object managed by this initializer class
|
||||
*/
|
||||
public abstract class AtomicInitializer<T> implements ConcurrentInitializer<T> {
|
||||
/** Holds the reference to the managed object. */
|
||||
private final AtomicReference<T> reference = new AtomicReference<T>();
|
||||
|
||||
/**
|
||||
* Returns the object managed by this initializer. The object is created if
|
||||
* it is not available yet and stored internally. This method always returns
|
||||
* the same object.
|
||||
*
|
||||
* @return the object created by this {@code AtomicInitializer}
|
||||
* @throws ConcurrentException if an error occurred during initialization of
|
||||
* the object
|
||||
*/
|
||||
public T get() throws ConcurrentException {
|
||||
T result = reference.get();
|
||||
|
||||
if (result == null) {
|
||||
result = initialize();
|
||||
if (!reference.compareAndSet(null, result)) {
|
||||
// another thread has initialized the reference
|
||||
result = reference.get();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes the object managed by this {@code
|
||||
* AtomicInitializer}. This method is called by {@link #get()} when the
|
||||
* managed object is not available yet. An implementation can focus on the
|
||||
* creation of the object. No synchronization is needed, as this is already
|
||||
* handled by {@code get()}. As stated by the class comment, it is possible
|
||||
* that this method is called multiple times.
|
||||
*
|
||||
* @return the managed data object
|
||||
* @throws ConcurrentException if an error occurs during object creation
|
||||
*/
|
||||
protected abstract T initialize() throws ConcurrentException;
|
||||
}
|
|
@ -82,7 +82,8 @@
|
|||
* @version $Id$
|
||||
* @param <T> the type of the object managed by this initializer class
|
||||
*/
|
||||
public abstract class BackgroundInitializer<T> {
|
||||
public abstract class BackgroundInitializer<T> implements
|
||||
ConcurrentInitializer<T> {
|
||||
/** The external executor service for executing tasks. */
|
||||
private ExecutorService externalExecutor;
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.commons.lang3.concurrent;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Definition of an interface for the thread-safe initialization of objects.
|
||||
* </p>
|
||||
* <p>
|
||||
* The idea behind this interface is to provide access to an object in a
|
||||
* thread-safe manner. A {@code ConcurrentInitializer} can be passed to multiple
|
||||
* threads which can all access the object produced by the initializer. Through
|
||||
* the {@link #getInitializedObject()} method the object can be queried.
|
||||
* </p>
|
||||
* <p>
|
||||
* Concrete implementations of this interface will use different strategies for
|
||||
* the creation of the managed object, e.g. lazy initialization or
|
||||
* initialization in a background thread. This is completely transparent to
|
||||
* client code, so it is possible to change the initialization strategy without
|
||||
* affecting clients.
|
||||
* </p>
|
||||
*
|
||||
* @author Apache Software Foundation
|
||||
* @version $Id$
|
||||
* @param <T> the type of the object managed by this initializer class
|
||||
*/
|
||||
public interface ConcurrentInitializer<T> {
|
||||
/**
|
||||
* Returns the fully initialized object produced by this {@code
|
||||
* ConcurrentInitializer}. A concrete implementation here returns the
|
||||
* results of the initialization process. This method may block until
|
||||
* results are available. Typically, once created the result object is
|
||||
* always the same.
|
||||
*
|
||||
* @return the object created by this {@code ConcurrentException}
|
||||
* @throws ConcurrentException if an error occurred during initialization of
|
||||
* the object
|
||||
*/
|
||||
T get() throws ConcurrentException;
|
||||
}
|
|
@ -164,6 +164,46 @@ private static void throwCause(ExecutionException ex) {
|
|||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Invokes the specified {@code ConcurrentInitializer} and returns the
|
||||
* object produced by the initializer. This method just invokes the {@code
|
||||
* get()} method of the given {@code ConcurrentInitializer}. It is
|
||||
* <b>null</b>-safe: if the argument is <b>null</b>, result is also
|
||||
* <b>null</b>.
|
||||
*
|
||||
* @param <T> the type of the object produced by the initializer
|
||||
* @param initializer the {@code ConcurrentInitializer} to be invoked
|
||||
* @return the object managed by the {@code ConcurrentInitializer}
|
||||
* @throws ConcurrentException if the {@code ConcurrentInitializer} throws
|
||||
* an exception
|
||||
*/
|
||||
public static <T> T initialize(ConcurrentInitializer<T> initializer)
|
||||
throws ConcurrentException {
|
||||
return (initializer != null) ? initializer.get() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the specified {@code ConcurrentInitializer} and transforms
|
||||
* occurring exceptions to runtime exceptions. This method works like
|
||||
* {@link #initialize(ConcurrentInitializer)}, but if the {@code
|
||||
* ConcurrentInitializer} throws a {@link ConcurrentException}, it is
|
||||
* caught, and the cause is wrapped in a {@link ConcurrentRuntimeException}.
|
||||
* So client code does not have to deal with checked exceptions.
|
||||
*
|
||||
* @param <T> the type of the object produced by the initializer
|
||||
* @param initializer the {@code ConcurrentInitializer} to be invoked
|
||||
* @return the object managed by the {@code ConcurrentInitializer}
|
||||
* @throws ConcurrentRuntimeException if the initializer throws an exception
|
||||
*/
|
||||
public static <T> T initializeUnchecked(ConcurrentInitializer<T> initializer) {
|
||||
try {
|
||||
return initialize(initializer);
|
||||
} catch (ConcurrentException cex) {
|
||||
throw new ConcurrentRuntimeException(cex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* <p>
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
* @version $Id$
|
||||
* @param <T> the type of the object managed by this initializer class
|
||||
*/
|
||||
public abstract class LazyInitializer<T> {
|
||||
public abstract class LazyInitializer<T> implements ConcurrentInitializer<T> {
|
||||
/** Stores the managed object. */
|
||||
private volatile T object;
|
||||
|
||||
|
@ -85,8 +85,10 @@ public abstract class LazyInitializer<T> {
|
|||
* is created. After that it is cached and can be accessed pretty fast.
|
||||
*
|
||||
* @return the object initialized by this {@code LazyInitializer}
|
||||
* @throws ConcurrentException if an error occurred during initialization of
|
||||
* the object
|
||||
*/
|
||||
public T get() {
|
||||
public T get() throws ConcurrentException {
|
||||
// use a temporary variable to reduce the number of reads of the
|
||||
// volatile field
|
||||
T result = object;
|
||||
|
@ -111,6 +113,7 @@ public T get() {
|
|||
* handled by {@code get()}.
|
||||
*
|
||||
* @return the managed data object
|
||||
* @throws ConcurrentException if an error occurs during object creation
|
||||
*/
|
||||
protected abstract T initialize();
|
||||
protected abstract T initialize() throws ConcurrentException;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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.commons.lang3.concurrent;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* An abstract base class for tests of concrete {@code ConcurrentInitializer}
|
||||
* implementations.
|
||||
* </p>
|
||||
* <p>
|
||||
* This class provides some basic tests for initializer implementations. Derived
|
||||
* class have to create a {@link ConcurrentInitializer} object on which the
|
||||
* tests are executed.
|
||||
* </p>
|
||||
*
|
||||
* @author Apache Software Foundation
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class AbstractConcurrentInitializerTest {
|
||||
/**
|
||||
* Tests a simple invocation of the get() method.
|
||||
*/
|
||||
@Test
|
||||
public void testGet() throws ConcurrentException {
|
||||
assertNotNull("No managed object", createInitializer().get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether sequential get() invocations always return the same
|
||||
* instance.
|
||||
*/
|
||||
@Test
|
||||
public void testGetMultipleTimes() throws ConcurrentException {
|
||||
ConcurrentInitializer<Object> initializer = createInitializer();
|
||||
Object obj = initializer.get();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertEquals("Got different object at " + i, obj, initializer.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether get() can be invoked from multiple threads concurrently.
|
||||
* Always the same object should be returned.
|
||||
*/
|
||||
@Test
|
||||
public void testGetConcurrent() throws ConcurrentException,
|
||||
InterruptedException {
|
||||
final ConcurrentInitializer<Object> initializer = createInitializer();
|
||||
final int threadCount = 20;
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
class GetThread extends Thread {
|
||||
Object object;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// wait until all threads are ready for maximum parallelism
|
||||
startLatch.await();
|
||||
// access the initializer
|
||||
object = initializer.get();
|
||||
} catch (InterruptedException iex) {
|
||||
// ignore
|
||||
} catch (ConcurrentException cex) {
|
||||
object = cex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetThread[] threads = new GetThread[threadCount];
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
threads[i] = new GetThread();
|
||||
threads[i].start();
|
||||
}
|
||||
|
||||
// fire all threads and wait until they are ready
|
||||
startLatch.countDown();
|
||||
for (Thread t : threads) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// check results
|
||||
Object managedObject = initializer.get();
|
||||
for (GetThread t : threads) {
|
||||
assertEquals("Wrong object", managedObject, t.object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link ConcurrentInitializer} object to be tested. This
|
||||
* method is called whenever the test fixture needs to be obtained.
|
||||
*
|
||||
* @return the initializer object to be tested
|
||||
*/
|
||||
protected abstract ConcurrentInitializer<Object> createInitializer();
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.commons.lang3.concurrent;
|
||||
|
||||
/**
|
||||
* Test class for {@code AtomicInitializer}.
|
||||
*
|
||||
* @author Apache Software Foundation
|
||||
* @version $Id$
|
||||
*/
|
||||
public class AtomicInitializerTest extends AbstractConcurrentInitializerTest {
|
||||
/**
|
||||
* Returns the initializer to be tested.
|
||||
*
|
||||
* @return the {@code AtomicInitializer}
|
||||
*/
|
||||
@Override
|
||||
protected ConcurrentInitializer<Object> createInitializer() {
|
||||
return new AtomicInitializer<Object>() {
|
||||
@Override
|
||||
protected Object initialize() throws ConcurrentException {
|
||||
return new Object();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@
|
|||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.easymock.EasyMock;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
|
@ -301,6 +302,75 @@ public void testHandleCauseUncheckedNull() throws ConcurrentException {
|
|||
null));
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Tests initialize() for a null argument.
|
||||
*/
|
||||
@Test
|
||||
public void testInitializeNull() throws ConcurrentException {
|
||||
assertNull("Got a result", ConcurrentUtils.initialize(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a successful initialize() operation.
|
||||
*/
|
||||
@Test
|
||||
public void testInitialize() throws ConcurrentException {
|
||||
@SuppressWarnings("unchecked")
|
||||
ConcurrentInitializer<Object> init = EasyMock
|
||||
.createMock(ConcurrentInitializer.class);
|
||||
final Object result = new Object();
|
||||
EasyMock.expect(init.get()).andReturn(result);
|
||||
EasyMock.replay(init);
|
||||
assertSame("Wrong result object", result, ConcurrentUtils
|
||||
.initialize(init));
|
||||
EasyMock.verify(init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests initializeUnchecked() for a null argument.
|
||||
*/
|
||||
@Test
|
||||
public void testInitializeUncheckedNull() {
|
||||
assertNull("Got a result", ConcurrentUtils.initializeUnchecked(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a successful initializeUnchecked() operation.
|
||||
*/
|
||||
@Test
|
||||
public void testInitializeUnchecked() throws ConcurrentException {
|
||||
@SuppressWarnings("unchecked")
|
||||
ConcurrentInitializer<Object> init = EasyMock
|
||||
.createMock(ConcurrentInitializer.class);
|
||||
final Object result = new Object();
|
||||
EasyMock.expect(init.get()).andReturn(result);
|
||||
EasyMock.replay(init);
|
||||
assertSame("Wrong result object", result, ConcurrentUtils
|
||||
.initializeUnchecked(init));
|
||||
EasyMock.verify(init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether exceptions are correctly handled by initializeUnchecked().
|
||||
*/
|
||||
@Test
|
||||
public void testInitializeUncheckedEx() throws ConcurrentException {
|
||||
@SuppressWarnings("unchecked")
|
||||
ConcurrentInitializer<Object> init = EasyMock
|
||||
.createMock(ConcurrentInitializer.class);
|
||||
final Exception cause = new Exception();
|
||||
EasyMock.expect(init.get()).andThrow(new ConcurrentException(cause));
|
||||
EasyMock.replay(init);
|
||||
try {
|
||||
ConcurrentUtils.initializeUnchecked(init);
|
||||
fail("Exception not thrown!");
|
||||
} catch (ConcurrentRuntimeException crex) {
|
||||
assertSame("Wrong cause", cause, crex.getCause());
|
||||
}
|
||||
EasyMock.verify(init);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Tests constant future.
|
||||
|
|
|
@ -16,82 +16,31 @@
|
|||
*/
|
||||
package org.apache.commons.lang3.concurrent;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import org.junit.Before;
|
||||
|
||||
/**
|
||||
* Test class for {@code LazyInitializer}.
|
||||
*
|
||||
* @version $Id$
|
||||
*/
|
||||
public class LazyInitializerTest extends TestCase {
|
||||
public class LazyInitializerTest extends AbstractConcurrentInitializerTest {
|
||||
/** The initializer to be tested. */
|
||||
private LazyInitializerTestImpl initializer;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
initializer = new LazyInitializerTestImpl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests obtaining the managed object.
|
||||
* Returns the initializer to be tested. This implementation returns the
|
||||
* {@code LazyInitializer} created in the {@code setUp()} method.
|
||||
*
|
||||
* @return the initializer to be tested
|
||||
*/
|
||||
public void testGet() {
|
||||
assertNotNull("No managed object", initializer.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether sequential get() invocations always return the same
|
||||
* instance.
|
||||
*/
|
||||
public void testGetMultipleTimes() {
|
||||
Object obj = initializer.get();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertEquals("Got different object at " + i, obj, initializer.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests invoking get() from multiple threads concurrently.
|
||||
*/
|
||||
public void testGetConcurrent() throws InterruptedException {
|
||||
final int threadCount = 20;
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
class GetThread extends Thread {
|
||||
Object object;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// wait until all threads are ready for maximum parallelism
|
||||
startLatch.await();
|
||||
// access the initializer
|
||||
object = initializer.get();
|
||||
} catch (InterruptedException iex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetThread[] threads = new GetThread[threadCount];
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
threads[i] = new GetThread();
|
||||
threads[i].start();
|
||||
}
|
||||
|
||||
// fire all threads and wait until they are ready
|
||||
startLatch.countDown();
|
||||
for (Thread t : threads) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// check results
|
||||
Object managedObject = initializer.get();
|
||||
for (GetThread t : threads) {
|
||||
assertEquals("Wrong object", managedObject, t.object);
|
||||
}
|
||||
@Override
|
||||
protected ConcurrentInitializer<Object> createInitializer() {
|
||||
return initializer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue