Reputation: 580
So I have this
@Value("classpath:choice-test.html")
private Resource sampleHtml;
private String sampleHtmlData;
@Before
public void readFile() throws IOException {
sampleHtmlData = IOUtils.toString(sampleHtml.getInputStream());
}
What I'd like to know is if it's possible to not have the readFile() method and have sampleHtmlData be injected with the contents of the file. If not I'll just have to live with this but it would be a nice shortcut.
Upvotes: 22
Views: 20691
Reputation: 1
Adopting to @Marcin Wisnicki it is possible to define an annotation say 'StringResource' with default value empty string and define utility class say 'StringResouceReader', a modified convertor such that:
The advantage of this approach is to make code more cleaner when there are multiple resources to be to be auto injected at different fields in same class. @StringResource(qualifier) can be specified on class and use @Value on as usual.
StringResource Class
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StringResource {
String value() default "";
}
StringResourceRender
@Service
public class StringResourceReader{
@Cacheable
public String getString(Resource resource){
return readAsString(resource);
}
private static String readAsString(Resource resource){
try {
return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static boolean isJavaIdentifier(String name){
if(name.isEmpty()){
return false;
}
char[] chars = name.toCharArray();
for(int i = 0;i<chars.length;i++){
if(i==0&&!Character.isJavaIdentifierStart(chars[i])){
return false;
}else if(!Character.isJavaIdentifierStart(chars[i])){
return false;
}
}
return true;
}
public static void registerConvertor(ConfigurableEnvironment environment, ResourceLoader resourceLoader){
environment.getConversionService().addConverter(new ConditionalGenericConverter() {
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if(targetType.hasAnnotation(Value.class)){
if(targetType.hasAnnotation(StringResource.class)){
return true;
}else{
Object source = targetType.getResolvableType().getSource();
if(source instanceof Member) {
Member member = (Member) source;
StringResource stringResource = AnnotationUtils.findAnnotation(member.getDeclaringClass(), StringResource.class);
if (stringResource != null) {
String qualifier = stringResource.value().trim();
if(qualifier.length()==0){
throw new IllegalStateException("Annotation StringResource must specify argument qualifier when used on a class");
}else if(!isJavaIdentifier(qualifier)){
throw new IllegalStateException("Qualifier must be java identifier");
}else{
return member.getName().startsWith(qualifier);
}
} else {
return false;
}
}else {
return false;
}
}
}else{
return false;
}
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return new HashSet(Arrays.asList(new GenericConverter.ConvertiblePair(String.class, String.class)));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return readAsString(resourceLoader.getResource(source.toString()));
}
});
}
}
Then register convertor in SpringApplication constructor
@SpringBootApplication
public class SomeApplicationApplication {
public SomeApplicationApplication(ConfigurableEnvironment environment, ResourceLoader resourceLoader){
StringResourceReader.registerConvertor(environment, resourceLoader);
}
public static void main(String[] args) {
SpringApplication.run(SomeApplicationApplication.class, args);
}
}
Upvotes: 0
Reputation: 341003
Technically you can do this with XML and an awkward combination of factory beans and methods. But why bother when you can use Java configuration?
@Configuration
public class Spring {
@Value("classpath:choice-test.html")
private Resource sampleHtml;
@Bean
public String sampleHtmlData() {
try(InputStream is = sampleHtml.getInputStream()) {
return IOUtils.toString(is, StandardCharsets.UTF_8);
}
}
}
Notice that I also close the stream returned from sampleHtml.getInputStream()
by using try-with-resources idiom. Otherwise you'll get memory leak.
Upvotes: 42
Reputation: 4701
If you want it reduced to one line per injection you can add annotation and conditional converter. This will also preserve ctrl-click navigation and autocomplete in IntelliJ.
@SpringBootApplication
public class DemoApplication {
@Value("classpath:application.properties")
@FromResource
String someFileContents;
@PostConstruct
void init() {
if (someFileContents.startsWith("classpath:"))
throw new RuntimeException("injection failed");
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
DemoApplication(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
// doing it in constructor ensures it executes before @Value injection in application
// if you don't inject values into application class, you can extract that to separate configuration
environment.getConversionService().addConverter(new ConditionalGenericConverter() {
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.hasAnnotation(FromResource.class);
}
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
return Set.of(new GenericConverter.ConvertiblePair(String.class, String.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
try (final var stream = resourceLoader.getResource(Objects.toString(source)).getInputStream()) {
return StreamUtils.copyToString(stream, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
});
}
}
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface FromResource {
}
Upvotes: 1
Reputation: 8281
There is no built-in functionality for this to my knowledge but you can do it yourself e.g. like this:
<bean id="fileContentHolder">
<property name="content">
<bean class="CustomFileReader" factory-method="readContent">
<property name="filePath" value="path/to/my_file"/>
</bean>
</property>
</bean>
Where readContent() returns a String which is read from the file on path/to/my_file.
Upvotes: 2