I am working on a project, where server side is based on Spring Boot 2, and front-end is based on Angular.
On the server side, in the data model classes I have declarations like that:
#Column(length = 256, nullable = false)
#Size(max = 256)
private String subject;
I would like to implement field length validation also on the front-end (angular side).
What is the best approach to share field length constraints between server and client side?
I do not like the idea that I need to repeat myself and hard-code field length on the both sides (server and client side).
Is it the best approach in my case if I declare set of constants like this:
private static final int maxSubjectLength = 256;
And use them as follows:
#Column(length = maxSubjectLength, nullable = false)
#Size(max = maxSubjectLength)
private String subject;
Then make a configuration class with these constants, which instance is accessible via GET http-request?
Or there is a better approach?
I decided to use the following approach. Assume we have a model class Question, that has a property body.
#Entity
#Table(name = "Questions")
public final class Question {
// other properties & code
private String body;
// other properties & code
}
And we want to limit the body length to 1024 symbols and define this limit only once, on the server and use this limit on the back-end and on the front-end of the application.
On the server side, in our model class Question we define static map, that contains size limits for all class properties.
#Entity
#Table(name = "Questions")
public final class Question {
private static class ModelConstraints {
static final int MAX_BODY_LENGTH = 1024;
// limits for other fields are going here
}
private static final Map<String, Integer> modelConstraintsMap;
static
{
final Map<String, Integer> localConstraintsMap = new HashMap<>();
localConstraintsMap.put("MAX_BODY_LENGTH", ModelConstraints.MAX_BODY_LENGTH);
// .... putting all constants from ModelConstraints to the map here
// composing unmodifable map
modelConstraintsMap = Collections.unmodifiableMap(localConstraintsMap);
}
#Column(length = ModelConstraints.MAX_BODY_LENGTH, nullable = false)
#Size(max = ModelConstraints.MAX_BODY_LENGTH)
private String body;
// other properties and code
public static Map<String, Integer> getModelConstraintsMap() {
return modelConstraintsMap;
}
// other properties and code
}
Internal class ModelConstraints contains definitions of max length values for all relevant model properties.
In the static block, I create an unmodifiable map, that contains these constraints and I return this map via public method.
In the controller, related to the model class I add a rest-endpoint that returns property length constraints.
#RequestMapping(path = "/questions/model-constraints", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<Map<String, Integer>> getModelConstraints() {
return new ResponseEntity<>(Question.getModelConstraintsMap(), HttpStatus.OK);
}
This method returns json-representation of the map with the property length constraints.
On the (angular) fron-tend I call this end-point and set maxlength property of form fields, related to the model class properties.
In the component typescript file I add and call this method:
loadConstraints() {
var url: string = "/questions/model-constraints";
this.http
.get(url)
.subscribe((data: Map<string, number>) => (this.modelConstraints = data));
}
And after call of this method the component property modelConstraints will contain map with field length constraints.
I set these constraints in the component template (html) file.
<textarea
matInput
rows="7"
placeholder="Question body"
maxlength="{{ modelConstraints['MAX_BODY_LENGTH'] }}"
[(ngModel)]="questionBody"
></textarea>
That's it. Using this approach you can define field length only once, on the server and use this definition on the server and on the client.
Related
I have kind of a combination-follow up question to [1] and [2].
I have a POJO with a field I want to persist in - and read from - Elasticsearch:
#Document
public class MyPojo {
private String level3;
// getters/setters...
}
For convenience and because the property is also being persisted (flatly) into postgres, the property level3 should be a String, however it should be written into ES as a nested object (because the ES index is defined elsewhere).
The current solution is unsatisfactory:
#Document
#Entity
public class MyPojo {
#Column(name = "level3)
#Field(name = "level3", type = FieldType.Keyword)
#ValueConverter(MyConverter.class)
private String level3;
// getters/setters...
}
with the object path "level1.level2.level3" hardcoded within MyConverter, which converts from Map<String, Object> to String (read) and from String to Map<String, Object> (write). Because we potentially need to do this on multiple fields, this is not a really viable solution.
I'd rather do something like this:
#Document
#Entity
public class MyPojo {
#Column(name = "level3)
#Field(name = "level1.level2.level3", type = FieldType.Keyword)
#ValueConverter(MyConverter2.class)
private String level3;
// getters/setters...
}
which does not work (writing works fine, while reading we get the "null is not a map" error from [2]).
Is this at all possible (if I understood [2] correctly, no)? If not, is there another way to achieve what I want without hardcoding and an extra converter per field?
Can I somehow access the #Field annotation within MyConverter (e.g. the name), or can I somehow supply additional arguments to MyConverter?
[1] Spring data elasticsearch embedded field mapping
[2] Spring Elasticsearch: Adding fields to working POJO class causes IllegalArgumentException: null is not a Map
Lets say I have a class as follows:
#JsonIgnoreProperties(ignoreUnknown = true)
class MyClass {
#JsonProperty(value="vertical")
private String vertical;
private String groupId;
#JsonProperty(value = "relationships")
private void unwrapGroupId(Map<String, Map<String, List<Map<String, Object>>>> relationships) {
this.groupId = ""; // Some logic to process the relationships map & set this.groupId based on the value set in this.vertical during deserialization
}
}
When deserializing an API Response to MyClass, is it guaranteed that vertical field is set before unwrapGroupId() is processed???? Else my processing in unwrapGroupId() would fail as this.vertical will be empty. If not , how can it be achieved.
I looked up #JsonPropertyOrder, but looks like it doesnt solve this usecase
Note: I use Jackson 2.8.1
Reporting is a big part of our requirements and I have been tasked with creating a generic reporting framework that allows the User to specify which columns they would like data from (across numerous tables), which conditions to apply, and which output format they want the data in.
I will need to store this information into a 'Template' object so that I can generate the same Report over and over with consistent results. Once I finish I will give the Users the ability to specify a Reoccurence option to automatically invoke their 'Template' daily, weekly, monthly, or annually if they choose to enable it.
I want to avoid taking the SQL String as input to remove the risk of SQL Injection and I got something working, but it seems like there can be a much better way than the way I am doing it currently.
I created 4 types of Java classes to construct the Query.
Query: This is what the User will provide specifying their SQL in JSON.
Filter: This is used to specify a condition to be applied to the query.
Select: This is used to specify a column to be returned from the result.
Join: This is used to specify that a join should connect another table.
Note: I am validating all Table Names and Field Names against the Hibernate Table and Column annotations to ensure they are valid.
Some things that are missing are the ability to use aliases and NOT clauses, which I will want to add later.
I am using mySQL at the moment and my query doesn't need to be database agnostic. If I need to rewrite it if I move to another vendor than so be it.
--
// This is my RequestBody
public class Query {
private String from;
private Filter filter;
private List<Join> joins;
private List<Select> selections;
--
#ApiModel(value="filter", discriminator = "type", subTypes = {
JoinerFilter.class, MultiFilter.class, SimpleFilter.class
})
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = JoinerFilter.class, name = "joiner"),
#JsonSubTypes.Type(value = MultiFilter.class, name = "multi"),
#JsonSubTypes.Type(value = SimpleFilter.class, name = "simple")
})
public abstract class Filter {
public abstract void validate();
public abstract String toSQL();
}
--
// This Filter is used to concatenate 2 Filters
#ApiModel(value = "joiner", parent = Filter.class)
public class JoinerFilter extends Filter {
private enum JoinerCondition {
AND, OR
}
private JoinerCondition condition;
private Filter lhsFilter;
private Filter rhsFilter;
--
// This Filter is used to perform a simple evaluation
#ApiModel(value = "simple", parent = Filter.class)
public class SimpleFilter extends Filter {
private enum SimpleCondition {
EQUAL, GREATER_THAN, LESS_THAN, LIKE
}
private String table;
private String field;
private String lhsFunction;
private String rhsFunction;
private SimpleCondition condition;
private String value;
--
// This Filter is used to search multiple values at once
#ApiModel(value = "multi", parent = Filter.class)
public class MultiFilter extends Filter {
private enum MultiCondition {
BETWEEN, IN
}
private String table;
private String field;
private String lhsFunction;
private String rhsFunction;
private MultiCondition condition;
private List<String> values;
--
public class Select {
private String table;
private String field;
private String function;
--
public class Join {
private enum JoinType {
INNER_JOIN, LEFT_JOIN, CROSS_JOIN
}
private Filter on;
private String table;
private JoinType type;
I am experimenting with spring data elasticsearch by implementing a cluster which will host multi-tenant indexes, one index per tenant.
I am able to create and set settings dynamically for each needed index, like
public class SpringDataES {
#Autowired
private ElasticsearchTemplate es;
#Autowired
private TenantIndexNamingService tenantIndexNamingService;
private void createIndex(String indexName) {
Settings indexSettings = Settings.builder()
.put("number_of_shards", 1)
.build();
CreateIndexRequest indexRequest = new CreateIndexRequest(indexName, indexSettings);
es.getClient().admin().indices().create(indexRequest).actionGet();
es.refresh(indexName);
}
private void preapareIndex(String indexName){
if (!es.indexExists(indexName)) {
createIndex(indexName);
}
updateMappings(indexName);
}
The model is created like this
#Document(indexName = "#{tenantIndexNamingService.getIndexName()}", type = "movies")
public class Movie {
#Id
#JsonIgnore
private String id;
private String movieTitle;
#CompletionField(maxInputLength = 100)
private Completion movieTitleSuggest;
private String director;
private Date releaseDate;
where the index name is passed dynamically via the SpEl
#{tenantIndexNamingService.getIndexName()}
that is served by
#Service
public class TenantIndexNamingService {
private static final String INDEX_PREFIX = "test_index_";
private String indexName = INDEX_PREFIX;
public TenantIndexNamingService() {
}
public String getIndexName() {
return indexName;
}
public void setIndexName(int tenantId) {
this.indexName = INDEX_PREFIX + tenantId;
}
public void setIndexName(String indexName) {
this.indexName = indexName;
}
}
So, whenever I have to execute a CRUD action, first I am pointing to the right index and then to execute the desired action
tenantIndexNamingService.setIndexName(tenantId);
movieService.save(new Movie("Dead Poets Society", getCompletion("Dead Poets Society"), "Peter Weir", new Date()));
My assumption is that the following dynamically index assignment, will not work correctly in a multi-threaded web application:
#Document(indexName = "#{tenantIndexNamingService.getIndexName()}"
This is because TenantIndexNamingService is singleton.
So my question is how achieve the right behavior in a thread save manner?
I would probably go with an approach similar to the following one proposed for Cassandra:
https://dzone.com/articles/multi-tenant-cassandra-cluster-with-spring-data-ca
You can have a look at the related GitHub repository here:
https://github.com/gitaroktato/spring-boot-cassandra-multitenant-example
Now, since Elastic has differences in how you define a Document, you should mainly focus in defining a request-scoped bean that will encapsulate your tenant-id and bind it to your incoming requests.
Here is my solution. I create a RequestScope bean to hold the indexes per HttpRequest
how does singleton bean handle dynamic index
I'm using Spring Boot and Freemarker. I'm loading my template from db using a custom loader:
public class ContentDbLoader implements TemplateLoader {
#Inject
private TemplateRepository templateRepository;
#Override
public void closeTemplateSource(Object templateSource) throws IOException {
return;
}
#Override
public Object findTemplateSource(String name) throws IOException {
return getTemplateFromName(name);
}
#Override
public long getLastModified(Object templateSource) {
MyTemplate template = (MyTemplate) templateSource;
template = templateRepository.findOne(template.getId());
return template.getLastModifiedDate().toEpochMilli();
}
#Override
public Reader getReader(Object templateSource, String encoding) throws IOException {
return new StringReader(((Template) templateSource).getContent());
}
private MyTemplate getTemplateFromName(String name) {
//my logic
}
}
My model is:
Entity
#Table(uniqueConstraints = { #UniqueConstraint(columnNames = { "channel", "communicationType", "country_id" }) })
public class Template extends AbstractEntity {
private static final long serialVersionUID = 8405971325717828643L;
#NotNull
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private Channel channel;
#NotNull
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private CommunicationType communicationType;
#Size(max = 255)
private String subject;
#NotNull
#Column(nullable = false)
#Lob
private String content;
#NotNull
#Column(nullable = false)
private String sender;
#NotNull
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Country country;
As you can see I return a MyTemplate object coming from db. When I get the custom template to precess the text, I do this:
contentFreemarkerConfig.getTemplate(CommunicationType.WALLET_BALANCE_THRESHOLD + "|" + Channel.EMAIL, locale)
but this line return a freemarker.template.Template. I would like to have my original template back in order to avoid to make another query on db to get it.
Is that possible with Freemarker?
What you try to do here (if I understand it well) is making FreeMarker to cache application domain objects for you. This is not something the standard template loading/caching mechanism was designed for; the template library is a lower level layer that deals with its FreeMarker Template-s only. As you need multiple Template-s per domain object, you can't even bolt this together with Template.customAttributes and a custom TemplateConfigurerFactory (which can be used to attach arbitrary objects to Template-s based on the templateSource). So, I think what you should do is this:
Use any dedicated caching solution to cache your domain objects. The domain objects can store the Template objects directly (multiple of them in your case). Simply create those Template objects with the Template constructor when you create the domain object for a cache miss. Thus for those Template-s FreeMarker's TemplateLoader and cache isn't used. As far as they don't need to #include or #import each other (and thus FreeMarker need to be able to get them), that's fine.
For the sake of templates that the above templates need to #include or #import (i.e., common utility templates), set up a TemplateLoader and FreeMarker's cache will be used for them. As those are just templates, not some application domain objects, certainly you don't need to do anything tricky. (Furthermore such template are often stored in classpath resources or in a configuration directory, so perhaps you don't even need a DatabaseTemplateLoader.)