Skip to content

Commit

Permalink
fix: store proxy beans now include a store translation interceptor th… (
Browse files Browse the repository at this point in the history
#1616)

* fix: store proxy beans now include a store translation interceptor that re-throws RuntimeException as StoreAccessException

- this provides a hook for stores to add their own translators

* Store exception translator interceptor now supports store-provided translations through StoreExceptionTranslator beans

* docs: add docs for StoreExceptionTranslator
  • Loading branch information
paulcwarren authored Oct 3, 2023
1 parent 5c93df3 commit 43150b4
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 169 deletions.
171 changes: 34 additions & 137 deletions spring-content-commons/src/main/asciidoc/content-repositories.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public class Application {

Currently, S3 is the only storage module that supports this experimental API.

=== Content Properties
== Content Properties

As we can see above content is "associated" by adding additional metadata about the content to the Entity. This additional metadata is annotated with Spring Content annotations. There are several. The only mandatory annotation is `@ContentId`. Other optional annotations include `@ContentLength`, `@MimeType` and `@OriginalFileName`. These may be added to your entities when you need to capture this additional infomation about your associated content.

Expand Down Expand Up @@ -389,7 +389,7 @@ store.setContent(user, PropertyPath.from("images/avatar"), avatarStream);
====

[[content-repositories.multimodule]]
=== Using Stores with Multiple Spring Content Storage Modules
== Using Stores with Multiple Spring Content Storage Modules

Using a single Spring Content storage module in your application keeps things simple because all Storage beans
will use to that one Spring Content storage module as their implementation. Sometimes, applications require
Expand All @@ -398,15 +398,15 @@ more than one Spring Content storage module. In such cases, a store definition

See <<signature_types,Signature Types>> for the signature types for the storage modules you are using.

==== Manual Storage Override
=== Manual Storage Override

Because Spring Content provides an abstraction over storage it is also common to use one storage module for testing but another
for production. For these cases it is possible to again include multiple Spring Content storage modules,
but use generic Store interfaces, rather than signature types, and instead specify the `spring.content.storage.type.default=<storage_module_id>`
property to manually set the storage implementation to be injected into your Storage beans.

[[content-repositories.events]]
=== Events
== Events

Spring Content emits twelve events. Roughly speaking one for each Store method. They are:

Expand Down Expand Up @@ -434,7 +434,7 @@ Spring Content emits twelve events. Roughly speaking one for each Store method.

* AfterUnsetContent

==== Writing an ApplicationListener
=== Writing an ApplicationListener

If you wish to extend Spring Content's functionality you can subclass the abstract class `AbstractStoreEventListener` and
override the methods that you are interested in. When these events occur your handlers will be called.
Expand Down Expand Up @@ -464,7 +464,7 @@ public class ExampleEventListener extends AbstractStoreEventListener {

The down-side of this approach is that it does not filter events based on Entity.

==== Writing an Annotated StoreEventHandler
=== Writing an Annotated StoreEventHandler

Another approach is to use an annotated handler, which does filter events based on Entity.

Expand Down Expand Up @@ -559,7 +559,7 @@ public class ContentStoreConfiguration {
====

[[content-repositories.search]]
=== Searchable Stores
== Searchable Stores
Applications that handle documents and other media usually have search capabilities allowing relevant content to be
found by looking inside of it for keywords or phrases, so called full-text search.

Expand Down Expand Up @@ -596,7 +596,7 @@ For `search` to return actual results full-text indexing must be enabled. See <
for more information on how to do this.

[[content-repositories.renditions]]
=== Renderable Stores
== Renderable Stores
Applications that handle files and other media usually also have rendition capabilities allowing content to be transformed
from one format to another.

Expand All @@ -617,6 +617,32 @@ Returns a `mimeType` rendition of the content associated with `entity`.
Renditions must be enabled and renderers provided. See <<renditions,Renditions>> for more
information on how to do this.

[[content-repositories.exceptions]]
== Error Translation

When using Stores, you must decide how to handle the storage technology’s native exception classes. Typically, storage layers throw runtime exceptions and do not have to be declared or caught. You may also have to deal with `IllegalArgumentException` and `IllegalStateException`. This means that callers can only treat exceptions as being generally fatal, unless they want to depend on the storage technology’s own exception structure. This trade-off might be acceptable to applications that are strongly aligned to a particular storage or do not need any special exception treatment (or both). However, Spring Content lets exception translation be applied transparently through the @Store annotations. The following examples show how to contribute a bean that implements `StoreExceptionTranslator` that translates RuntimeException's to StoreAccessExceptions:

.StoreExceptionTranslator interface
====
[source,java]
----
@Configuration
public class Config {
@Bean
public StoreExceptionTranslator translator() {
return new StoreExceptionTranslator() {
@Override
public StoreAccessException translate(RuntimeException re) {
...
}
};
}
InputStream getRendition(E entity, String mimeType);
}
----
====

[[content-repositories.creation]]
== Creating Content Store Instances
To use these core concepts:
Expand Down Expand Up @@ -707,132 +733,3 @@ public class SomeClass {
----
<1> Spring Content will update the `@ContentId` and `@ContentLength` fields
====

== Patterns of Content Association

Content can be associated with a Spring Data Entity in several ways.

=== Entity Association

The simplest, allowing you to associate one Entity with one Resource, is to decorate your Spring Data Entity with the Spring Content attributes.

The following example shows a Resource associated with an Entity `Dvd`.

====
[source, java]
----
@Entity
public class Dvd {
private @Id @GeneratedValue Long id;
private String title;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
...
}
public interface DvdRepository extends CrudRepository<Dvd, Long> {}
public interface DvdStore extends ContentStore<Dvd, UUID> {}
----
====

=== Property Association

Sometimes you might want to associate multiple different Resources with an Entity. To do this it is also possible to associate Resources with one or more Entity properties.

The following example shows two Resources associated with a `Dvd` entity. The first Resource is the Dvd's cover Image and the second is the Dvd's Stream.

====
[source, java]
----
@Entity
public class Dvd {
private @Id @GeneratedValue Long id;
private String title;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "image_id")
private Image image;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "stream_id")
private Stream stream;
...
}
@Entity
public class Image {
// Spring Data managed attribute
private @Id @GeneratedValue Long id;
@OneToOne
private Dvd dvd;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
}
@Entity
public class Stream {
// Spring Data managed attribute
private @Id @GeneratedValue Long id;
@OneToOne
private Dvd dvd;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
}
public interface DvdRepository extends CrudRepository<Dvd, Long> {}
public interface ImageStore extends ContentStore<Image, UUID> {}
public interface StreamStore extends ContentStore<Stream, UUID> {}
----
====

Note how the Content attributes are placed on each property object of on the Entity itself.

When using JPA with a relational database these are typically (but not always) also Entity associations. However when using NoSQL databases like MongoDB that are capable of storing hierarchical data they are true property associations.

==== Property Collection Associations

In addition to associating many different types of Resource with a single Entity. It is also possible to associate one Entity with many Resources using a `java.util.Collection` property, as the following example shows.

====
[source, java]
----
@Entity
public class Dvd {
private @Id @GeneratedValue Long id;
private String title;
@OneToMany
@JoinColumn(name = "chapter_id")
private List<Chapter> chapters;
...
}
@Entity
public class Chapter {
// Spring Data managed attribute
private @Id @GeneratedValue Long id;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
}
public interface DvdRepository extends CrudRepository<Dvd, Long> {}
public interface ChapterStore extends ContentStore<Chapter, UUID> {}
----
====
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package internal.org.springframework.content.commons.store.factory;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.content.commons.store.StoreAccessException;
import org.springframework.content.commons.store.StoreExceptionTranslator;

import java.util.ArrayList;
import java.util.List;

public class StoreExceptionTranslatorInterceptor implements MethodInterceptor {
private final BeanFactory beanFactory;
private List<StoreExceptionTranslator> translators = null;

public StoreExceptionTranslatorInterceptor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
return invocation.proceed();
} catch (RuntimeException re) {
if (re instanceof StoreAccessException) {
throw re;
}
if (translators == null) {
translators = new ArrayList<>();
beanFactory.getBeanProvider(StoreExceptionTranslator.class).orderedStream().forEach(translators::add);
}
StoreAccessException sae = null;
for (int i=0; i < translators.size() && sae == null; i++) {
sae = translators.get(i).translate(re);
}
throw sae != null ? sae : re;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.util.Map;
import java.util.Set;

import internal.org.springframework.content.commons.store.factory.StoreFactory;
import internal.org.springframework.content.commons.store.factory.*;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -33,9 +33,6 @@

import internal.org.springframework.content.commons.config.StoreFragment;
import internal.org.springframework.content.commons.config.StoreFragments;
import internal.org.springframework.content.commons.store.factory.ReactiveStoreImpl;
import internal.org.springframework.content.commons.store.factory.StoreImpl;
import internal.org.springframework.content.commons.store.factory.StoreMethodInterceptor;

/**
* @deprecated This class is deprecated. Use {@link org.springframework.content.commons.store.factory.AbstractStoreFactoryBean} instead.
Expand Down Expand Up @@ -225,6 +222,7 @@ protected Store<? extends Serializable> createContentStore() {
}
intercepter.setStoreFragments(storeFragments);

result.addAdvice(new StoreExceptionTranslatorInterceptor(beanFactory));
result.addAdvice(intercepter);

return (Store<? extends Serializable>) result.getProxy(classLoader);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.springframework.content.commons.store;

@FunctionalInterface
public interface StoreExceptionTranslator {
StoreAccessException translate(RuntimeException re);
}
Loading

0 comments on commit 43150b4

Please sign in to comment.