mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-10-26 00:56:09 +02:00
Add queryable store with SQLite implementation
This adds the new "queryable store" API, that allows complex queries and is backed by SQLite. This new API can be used for entities annotated with the new QueryableType annotation.
This commit is contained in:
452
docs/en/development/storage.md
Normal file
452
docs/en/development/storage.md
Normal file
@@ -0,0 +1,452 @@
|
||||
---
|
||||
title: Architecture of the Persistence Layer with Embedded SQLite
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
In SCM-Manager, data outside the actual repositories has been stored in XML files since version 2.0. For this purpose, a
|
||||
persistence layer was developed that allows various types of data to be stored. It is also possible to choose whether
|
||||
data should be stored globally or associated with a repository.
|
||||
|
||||
This type of storage has generally proven effective and offers several advantages (data is stored with the repository,
|
||||
easy troubleshooting, simple backup, etc.). However, with large amounts of data or frequently changing data, this
|
||||
architecture reaches its limits. Several optimizations have been made (e.g., through caches), but the fundamental
|
||||
limitations remain. In particular, searches are difficult because many files have to be read and processed.
|
||||
|
||||
It was therefore necessary to look for new possibilities. The fundamental advantages of SCM-Manager should remain,
|
||||
especially the easy installation, simple operation, the ability to easily transfer repositories between different
|
||||
instances through export and import, and not least the easy use of the persistence layer by plugins.
|
||||
|
||||
It quickly became clear that using a database system would be a sensible alternative. However, it was uncertain whether it
|
||||
should be a "classic" database or a NoSQL database. XML storage had proven to be very helpful, as the only prerequisite for
|
||||
a persistent data type was the use of JaxB annotations. However, a widely recognized technology should also be used.
|
||||
|
||||
The choice finally fell on SQLite. This system is available for almost every platform and databases can be used "
|
||||
embedded", so no separate server process is needed. The deciding factor was the performance, which is also present with
|
||||
embedded JSON data.
|
||||
|
||||
The next point to clarify was how the abstraction should look. It was clear from the beginning that plugins should not
|
||||
directly access databases via SQL. Rather, the API for persistence should be oriented towards the XML-based solution.
|
||||
|
||||
The following sections introduce the most important concepts.
|
||||
|
||||
# Important Components of the Architecture
|
||||
|
||||
## Objectives
|
||||
|
||||
The following aspects were decisive in introducing the new persistence layer:
|
||||
|
||||
- Primarily, an alternative should be developed for the existing Data Store, as most data is stored there.
|
||||
Configuration stores are unlikely to pose a performance problem.
|
||||
- The specific choice of database should not be noticeable in the API, so that a change of the specific technology
|
||||
remains possible in principle.
|
||||
- The API for using the new persistence layer should be as similar as possible to the existing API. In particular, it
|
||||
should not be necessary to create a mapping from the entities to be stored to a database schema (like an OR mapping).
|
||||
- As an extension to the XML layer, it should be possible to store data not only globally or related to a repository or
|
||||
namespace but also to allow other hierarchies (even those that may only arise through plugins and cannot be
|
||||
anticipated in the API).
|
||||
- Unlike XML persistence, queries should be possible that span multiple entries. Additionally, for entities assigned to
|
||||
individual repositories, queries should also be able to cross repository boundaries.
|
||||
- The API should, especially for queries, provide the best possible options, such as which fields can be searched and
|
||||
which operators are possible for these fields.
|
||||
- The previous functions such as export and import with metadata, update steps, and automatic data cleanup,
|
||||
for example when deleting a repository, should remain available.
|
||||
- A switch from the old to the new persistence layer should be as simple as possible.
|
||||
- The principle "All data belonging to a repository is in a single directory" can be relaxed for performance reasons.
|
||||
|
||||
## Annotations and API Generation
|
||||
|
||||
To achieve the best possible "Developer Experience", code generation is used. This is triggered by using a new
|
||||
annotation for persistent entities.
|
||||
|
||||
### The "Queryable Type"
|
||||
|
||||
The `@QueryableType` annotation is the central element of the persistence architecture. It allows classes to be marked
|
||||
for use in SQL-based database queries. In the annotation, so-called parent classes can be listed to which the entities
|
||||
should later belong. For a repository-related type, the `Repository` class must be entered here. Multiple classes can
|
||||
also be specified here in the sense of a hierarchy (e.g., a comment can belong to a pull request, which in turn belongs
|
||||
to a repository).
|
||||
|
||||
For such marked classes, additional classes are automatically generated: a Store Factory and a class with constants for
|
||||
the individual fields that can be used in queries (the "Query Fields").
|
||||
|
||||
### Store Factories and Stores
|
||||
|
||||
The generated Store Factories are similar to the known `DataStoreFactory`. Unlike the generic `DataStoreFactory`,
|
||||
however, specific methods are created here based on the parent classes mentioned in the annotation.
|
||||
|
||||
To create and change data, specific IDs must be specified to access the store if parent classes have been defined. This
|
||||
store implements the known Store API (the `DataStore` interface), so no adjustments are needed in the application.
|
||||
|
||||
### Queryable Store
|
||||
|
||||
For more advanced queries that also extend beyond the boundaries of the parent classes, there is a new store with a new
|
||||
API, the `QueryableStore`. This offers a `query` function in which conditions can be specified and a query can be
|
||||
started. The conditions are based on the generated Queryable Fields described below.
|
||||
|
||||
### Queryable Mutable Store
|
||||
|
||||
To store, delete, and change data, a new store with the `QueryableMutableStore` API is used. This API
|
||||
extends `QueryableStore` and `DataStore` to allow both queries and changes to stored objects. In contrast to the
|
||||
pure Queryable Store, it is mandatory to specify all parents to create a mutable store. This is needed so that new
|
||||
entities can be assigned to the correct parent(s).
|
||||
|
||||
### Queryable Maintenance Store
|
||||
|
||||
The `QueryableMaintenanceStore` is responsible for maintenance tasks,
|
||||
such as deleting all data of a specific type or updating stored JSON data.
|
||||
|
||||
- One use case is deleting a parent ID (e.g., repository ID):
|
||||
|
||||
For example, if a repository is deleted, all entries with this ID as the parent ID must also be removed. This automatic
|
||||
cleanup is ensured by the `QueryableMaintenanceStore`.
|
||||
With the `clear()` function, all entries of a specific type can be specifically removed.
|
||||
|
||||
- Another use case is "update steps". Here, all entries of a store can be iterated and potentially updated or deleted using
|
||||
the `QueryableMaintenanceStore`.
|
||||
|
||||
### Queryable Fields
|
||||
|
||||
The individually generated Queryable Fields for a "Queryable Type" are a collection of constants that can be used to
|
||||
define conditions for queries over Queryable Stores. For all attributes of the Queryable Type with supported data types,
|
||||
a corresponding constant is generated. These offer functions for operators such as equality, greater and less for scalar
|
||||
values, or "contains" for collections, depending on the data type.
|
||||
|
||||
The generated store factories described in the previous section restrict the usable queryable fields per generic to
|
||||
prevent incorrect queries from being created.
|
||||
|
||||
### Queryable Type Annotation Processor
|
||||
|
||||
The `QueryableTypeAnnotationProcessor` is an annotation processor that automatically generates SQL-related classes
|
||||
during compilation. It identifies classes annotated with `@QueryableType` and creates corresponding `QueryField` classes
|
||||
and Store Factories.
|
||||
|
||||
Functions:
|
||||
|
||||
- Identification of classes annotated with `@QueryableType`
|
||||
- Generation of Query Field classes and Store Factories
|
||||
|
||||
## Implementation in the Database
|
||||
|
||||
When the SCM-Manager starts, an embedded SQLite database is set up. This is stored in the `scm.db` file in the SCM home
|
||||
directory. Additionally, during startup, a table is created in the database for each queryable type if it does not already exist.
|
||||
Each table includes the following columns:
|
||||
|
||||
- A column for the ID of each parent level
|
||||
- A column for the ID of the actual entity
|
||||
- A column containing the entity converted to JSON
|
||||
|
||||
### Rules for the Database Structure
|
||||
|
||||
- The existing table structure must not be changed.
|
||||
- No new parent classes (parents) may be added to or removed from an existing entity.
|
||||
- The JSON data within the existing column may be updated to make changes to the stored entities.
|
||||
|
||||
These restrictions ensure that the integrity of the database structure is maintained and migrations can be performed
|
||||
without manual adjustments to the schema definition.
|
||||
|
||||
### Table Creation with the TableCreator
|
||||
|
||||
The `TableCreator` class is responsible for creating and validating the table structure. It checks whether a table
|
||||
exists and whether the required columns (ID, JSON, and specific columns for the parents) are present.
|
||||
|
||||
The implementation ensures that only consistent table structures are created and used.
|
||||
|
||||
### Implementation of StoreFactory and Stores
|
||||
|
||||
#### SQLiteQueryableStoreFactory
|
||||
|
||||
The `SQLiteQueryableStoreFactory` class is the concrete implementation of `QueryableStoreFactory` for SQLite databases.
|
||||
|
||||
Functions:
|
||||
|
||||
- Management of SQLite database connections:
|
||||
- Connects the application to the SQLite database (`scm.db`).
|
||||
- Ensures that the connection is correctly opened and closed.
|
||||
- Table initialization:
|
||||
- Tables are automatically created based on the metadata of `@QueryableType`.
|
||||
- Creation of stores:
|
||||
- Supports both reading (`QueryableStore`) and writing (`QueryableMutableStore`) stores as well as stores for
|
||||
maintenance (`QueryableMaintenanceStore`).
|
||||
|
||||
#### SQLiteQueryableStore
|
||||
|
||||
`SQLiteQueryableStore` is a generic implementation of `QueryableStore` that abstracts SQL logic and seamlessly
|
||||
integrates into the persistence architecture.
|
||||
|
||||
Purpose and scope:
|
||||
|
||||
- Abstraction of SQL logic:
|
||||
- Developers define queries in an object-oriented manner without having to write SQL directly.
|
||||
- Integration with SQLite:
|
||||
- Uses a JDBC connection to perform database operations.
|
||||
- Data management:
|
||||
- Supports reading queries on persisted data defined by annotations such as `@QueryableType`.
|
||||
- Architecture and operation:
|
||||
- Metadata integration:
|
||||
|
||||
Uses `QueryableTypeDescriptor` to interpret table structure and relationships.
|
||||
|
||||
*Note:* The parents of an already existing `QueryableType` must not be changed (new ones added or old ones removed)
|
||||
as this would differ from the existing database structure and could lead to errors.
|
||||
Declarative queries: Queries are created and internally translated into SQL. Results are mapped to objects of type
|
||||
T.
|
||||
|
||||
#### SQLiteStoreMetadataProvider
|
||||
|
||||
The `SQLiteStoreMetaDataProvider` class serves as a provider of metadata for stored types within the SQLite database. It
|
||||
manages the mapping of stored entity types to their respective parent types and provides mechanisms for querying this
|
||||
information. The use case is to be able to recognize which tables are repository-related, i.e., which tables have a
|
||||
repository as a parent.
|
||||
|
||||
Functions:
|
||||
|
||||
- Loading metadata:
|
||||
|
||||
When initializing, all types annotated with `@QueryableType` are loaded and registered.
|
||||
The information comes from the `PluginLoader` and is organized based on the specified parent classes.
|
||||
- Management of the type hierarchy:
|
||||
|
||||
Stores the mapping between parent types and their subordinate types in a map.
|
||||
- Retrieval of types based on parent classes:
|
||||
|
||||
Provides a method for querying all entity types associated with a specific parent class.
|
||||
Uses a mapping list (`Map<Collection<String>, Collection<Class<?>>>`) to enable efficient searching for stored types.
|
||||
|
||||
This class is essential for the correct management of stored data types in the SQLite database and ensures that the data
|
||||
hierarchy can be correctly built and queried.
|
||||
|
||||
#### StoreDeletionNotifier
|
||||
|
||||
The `StoreDeletionNotifier` interface serves as an extension point (`@ExtensionPoint`) to notify components about the
|
||||
deletion of persisted objects.
|
||||
|
||||
Functions:
|
||||
|
||||
- Registration of deletion handlers:
|
||||
|
||||
Allows the registration of `DeletionHandler` instances that should be notified when a stored object is deleted.
|
||||
- Notification of deleted entities:
|
||||
|
||||
`DeletionHandler` can receive deletion events and react to them.
|
||||
|
||||
Supports both single and multiple objects to be deleted.
|
||||
|
||||
Inner components:
|
||||
|
||||
- `DeletionHandler`
|
||||
Is notified when an object is removed from the store.
|
||||
|
||||
This interface is essential to ensure consistent management of deleted entities and can be used, for example, to remove
|
||||
dependent data or perform actions after an object is deleted from the store.
|
||||
|
||||
## Testability
|
||||
|
||||
To support unit tests, there is an extension for JUnit Jupiter, the `QueryableStoreExtension`. In a unit test, this must
|
||||
be specified in a JUnit extension annotation. Additionally, the test class must be annotated
|
||||
with `QueryableStoreExtension#QueryableTypes` to specify which types are needed in the test. Subsequently, it is
|
||||
possible to obtain store factories via parameters to test methods (or also to methods annotated with `@BeforeEach`).
|
||||
|
||||
# Examples
|
||||
|
||||
## Using the New Queryable Store API
|
||||
|
||||
First, a data type must be marked as a "Queryable Type":
|
||||
|
||||
```java
|
||||
import lombok.Data;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@Data
|
||||
@QueryableType
|
||||
public class MyEntity {
|
||||
private String id;
|
||||
private String name;
|
||||
private String alias;
|
||||
private int age;
|
||||
private List<String> tags;
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the entity has no relation to parent elements. The `@QueryableType` annotation is sufficient to store
|
||||
the entity in the database. During compilation, the following classes are automatically generated:
|
||||
|
||||
- `MyEntityQueryFields`: Constants for the fields that can be used in queries
|
||||
- `MyEntityStoreFactory`: Factory for accessing the store
|
||||
|
||||
Using these classes, data can then be stored and queried as shown in the following example:
|
||||
|
||||
```java
|
||||
public class Demo {
|
||||
|
||||
private final MyEntityStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
public Demo(MyEntityStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public String create(String name, int age, List<String> tags) {
|
||||
MyEntity entity = new MyEntity();
|
||||
entity.setName(name);
|
||||
entity.setAge(age);
|
||||
entity.setTags(tags);
|
||||
|
||||
QueryableMutableStore<MyEntity> store = storeFactory.getMutable();
|
||||
return store.put(entity);
|
||||
}
|
||||
|
||||
public MyEntity readById(String id) {
|
||||
QueryableMutableStore<MyEntity> store = storeFactory.getMutable();
|
||||
return store.get(id);
|
||||
}
|
||||
|
||||
public Collection<MyEntity> findByAge(int age) {
|
||||
QueryableStore<MyEntity> store = storeFactory.get();
|
||||
return store.query(MyEntityQueryFields.AGE.eq(age)).findAll();
|
||||
}
|
||||
|
||||
public Collection<MyEntity> findByName(String name) {
|
||||
QueryableStore<MyEntity> store = storeFactory.get();
|
||||
return store.query(
|
||||
Conditions.or(
|
||||
MyEntityQueryFields.NAME.eq(name),
|
||||
MyEntityQueryFields.ALIAS.eq(name)
|
||||
)
|
||||
).findAll();
|
||||
}
|
||||
|
||||
public Collection<MyEntity> findByTag(String tag) {
|
||||
QueryableStore<MyEntity> store = storeFactory.get();
|
||||
return store.query(MyEntityQueryFields.TAGS.contains(tag)).findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using the Queryable Store API with Parent Element
|
||||
|
||||
Consider the following example with a parent element where we want to store multiple contacts for a user:
|
||||
|
||||
```java
|
||||
@Data
|
||||
@QueryableType(User.class)
|
||||
public class Contact {
|
||||
private String mail;
|
||||
}
|
||||
```
|
||||
|
||||
For entities with parent elements, queries can be made both for specific parents and across all parents.
|
||||
|
||||
```java
|
||||
public class Demo {
|
||||
|
||||
private final ContactStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
public Demo(ContactStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public void addContact(User user, String mail) {
|
||||
QueryableMutableStore<Contact> store = storeFactory.getMutable(user);
|
||||
Contact contact = new Contact();
|
||||
contact.setMail(mail);
|
||||
store.put(contact);
|
||||
}
|
||||
|
||||
/** Get contact for a single user. */
|
||||
public Collection<Contact> getContacts(User user) {
|
||||
QueryableMutableStore<Contact> store = storeFactory.getMutable(user);
|
||||
return store.getAll().values();
|
||||
}
|
||||
|
||||
/** Get all contacts for all users. */
|
||||
public Collection<Contact> getAllContacts() {
|
||||
QueryableStore<Contact> store = storeFactory.getOverall();
|
||||
return store.query().findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, all `Contact` entries will be deleted, when the related `User` is deleted. This works out-of-the-box
|
||||
for all entities whose top level parent is a `User`, a `Group`, or a `Repository`. You can build this behavior for your
|
||||
own parent types by implementing a `StoreDeletionNotifier` as an extension. Best take a look at the `GroupDeletionNotifier`
|
||||
for an example:
|
||||
|
||||
```java
|
||||
@Extension
|
||||
public class GroupDeletionNotifier implements StoreDeletionNotifier {
|
||||
private DeletionHandler handler;
|
||||
|
||||
@Override
|
||||
public void registerHandler(DeletionHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@Subscribe(referenceType = ReferenceType.STRONG)
|
||||
public void onDelete(GroupEvent event) {
|
||||
if (handler != null && event.getEventType() == HandlerEventType.DELETE) {
|
||||
handler.notifyDeleted(Group.class, event.getItem().getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Update Steps
|
||||
|
||||
Update steps can be used to update data in the database. The following example shows how to update all entities of a
|
||||
specific type. For this let's assume, that we want to add a `type` field to the `Contact` entity from the previous
|
||||
example:
|
||||
|
||||
```java
|
||||
@Data
|
||||
@QueryableType(User.class)
|
||||
public class Contact {
|
||||
private String mail;
|
||||
private String type;
|
||||
}
|
||||
```
|
||||
|
||||
The following update step can be used to add the `type` field to all `Contact` entities:
|
||||
|
||||
```java
|
||||
@Extension
|
||||
public class AddTypeToContactsUpdateStep implements UpdateStep {
|
||||
|
||||
private final StoreUpdateStepUtilFactory updateStepUtilFactory;
|
||||
|
||||
@Inject
|
||||
public AddTypeToContactsUpdateStep(StoreUpdateStepUtilFactory updateStepUtilFactory) {
|
||||
this.updateStepUtilFactory = updateStepUtilFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() {
|
||||
try (MaintenanceIterator<Contact> iter = updateStepUtilFactory.forQueryableType(Contact.class).iterateAll()) {
|
||||
while(iter.hasNext()) {
|
||||
MaintenanceStoreEntry<Contact> entry = iter.next();
|
||||
Contact contact = entry.get();
|
||||
contact.setType("personal");
|
||||
entry.update(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return Version.parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "userContacts";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Please note that the iterator from the `StoreUpdateStepUtilFactory` has to be closed after usage. This is done best with
|
||||
a try-with-resources block like in the example above.
|
||||
|
||||
If the new entity differs in a significant way so that the old stored data can no longer be read from the store using
|
||||
the new entity, you can use the method `entry#getAs(Class<T>)` with a class that matches the old structure of the entity
|
||||
and use this to create a new entity that can be stored with the new structure.
|
||||
2
gradle/changelog/queryable.yaml
Normal file
2
gradle/changelog/queryable.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: New store API with enhanced query options backed by SQLite
|
||||
@@ -15,6 +15,7 @@ ext {
|
||||
bouncycastleVersion = '2.73.6'
|
||||
jettyVersion = '11.0.24'
|
||||
luceneVersion = '8.11.4'
|
||||
sqliteVersion = '3.49.1.0'
|
||||
|
||||
junitJupiterVersion = '5.10.3'
|
||||
hamcrestVersion = '3.0'
|
||||
@@ -193,6 +194,9 @@ ext {
|
||||
|
||||
// metrics
|
||||
micrometerCore: "io.micrometer:micrometer-core:${micrometerVersion}",
|
||||
micrometerExtra: "io.github.mweirauch:micrometer-jvm-extras:0.2.2"
|
||||
micrometerExtra: "io.github.mweirauch:micrometer-jvm-extras:0.2.2",
|
||||
|
||||
// SQLite
|
||||
sqlite: "org.xerial:sqlite-jdbc:${sqliteVersion}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,24 @@ dependencies {
|
||||
// utils
|
||||
implementation libraries.guava
|
||||
|
||||
implementation "com.google.auto:auto-common:1.2.2"
|
||||
|
||||
implementation 'com.squareup:javapoet:1.13.0'
|
||||
|
||||
testImplementation "com.google.testing.compile:compile-testing:0.21.0"
|
||||
testImplementation libraries.junitJupiterApi
|
||||
testImplementation libraries.junitJupiterEngine
|
||||
testImplementation libraries.assertj
|
||||
|
||||
// service registration
|
||||
compileOnly libraries.metainfServices
|
||||
annotationProcessor libraries.metainfServices
|
||||
|
||||
}
|
||||
|
||||
test {
|
||||
// See: https://github.com/google/compile-testing/issues/222
|
||||
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")
|
||||
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED")
|
||||
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import sonia.scm.plugin.PluginAnnotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* This annotation is used to mark a class as queryable type. Classes annotated with this annotation can be stored in
|
||||
* a store and later be queried more efficiently and with more flexibility than with a simple key-value store.
|
||||
* <br/>
|
||||
* If the annotation is used without any parameters, the class name is used as the name of the queryable type and
|
||||
* the objects of this class will be stored with ids (either given or generated by the store) independently of any
|
||||
* other (parent) objects. If the objects are related to other objects, the parent objects can be specified with the
|
||||
* {@link #value()} parameter. The parent objects are used to create a hierarchy of objects (formerly it only was
|
||||
* possible to store objects related to repositories; with this annotation it is possible to use other objects as
|
||||
* parents, too, like for instance users).
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@PluginAnnotation("queryable-type")
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface QueryableType {
|
||||
/**
|
||||
* The parent types of the queryable type. The parent objects are used to create a hierarchy of objects. If no parent
|
||||
* types are specified, the type is stored independently of any other objects.
|
||||
*/
|
||||
Class<?>[] value() default {};
|
||||
|
||||
/**
|
||||
* This can be used to specify a name for the queryable type. If no name is specified, the class name is used as the
|
||||
* name of the queryable type.
|
||||
*/
|
||||
String name() default "";
|
||||
}
|
||||
50
scm-core-annotation-processor/build.gradle
Normal file
50
scm-core-annotation-processor/build.gradle
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.scm-manager.java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform(project(':'))
|
||||
implementation project(':scm-annotations')
|
||||
implementation project(':scm-core')
|
||||
|
||||
implementation "com.google.auto:auto-common:1.2.2"
|
||||
|
||||
implementation 'com.squareup:javapoet:1.13.0'
|
||||
|
||||
compileOnly libraries.lombok;
|
||||
annotationProcessor libraries.lombok;
|
||||
|
||||
testImplementation "com.google.testing.compile:compile-testing:0.21.0"
|
||||
testImplementation libraries.junitJupiterApi
|
||||
testImplementation libraries.junitJupiterParams
|
||||
testImplementation libraries.junitJupiterEngine
|
||||
testImplementation libraries.assertj
|
||||
|
||||
// service registration
|
||||
compileOnly libraries.metainfServices
|
||||
annotationProcessor libraries.metainfServices
|
||||
}
|
||||
|
||||
test {
|
||||
// See: https://github.com/google/compile-testing/issues/222
|
||||
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")
|
||||
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED")
|
||||
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
class AnnotationHelper {
|
||||
|
||||
public Optional<? extends AnnotationValue> findAnnotationValue(AnnotationMirror annotationMirror, String name) {
|
||||
return annotationMirror.getElementValues()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> entry.getKey().getSimpleName().toString().equals(name))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.Element;
|
||||
import java.util.Optional;
|
||||
|
||||
class AnnotationProcessor {
|
||||
Optional<? extends AnnotationMirror> findAnnotation(Element element, Class<?> annotationClass) {
|
||||
return element.getAnnotationMirrors()
|
||||
.stream()
|
||||
.filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(annotationClass.getCanonicalName()))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.JavaFile;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import jakarta.inject.Inject;
|
||||
import sonia.scm.ModelObject;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
class FactoryClassCreator {
|
||||
|
||||
private static final String STORE_PACKAGE_NAME = "sonia.scm.store";
|
||||
private static final String QUERYABLE_MUTABLE_STORE_CLASS_NAME = "QueryableMutableStore";
|
||||
private static final String QUERYABLE_STORE_CLASS_NAME = "QueryableStore";
|
||||
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
|
||||
FactoryClassCreator(ProcessingEnvironment processingEnv) {
|
||||
this.processingEnv = processingEnv;
|
||||
}
|
||||
|
||||
void createFactoryClass(Element element, String packageName, TypeElement dataClassTypeElement) throws IOException {
|
||||
TypeName typeNameOfDataClass = TypeName.get(dataClassTypeElement.asType());
|
||||
TypeSpec.Builder builder =
|
||||
TypeSpec
|
||||
.classBuilder(element.getSimpleName() + "StoreFactory")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addJavadoc("Generated queryable store factory for type {@link $T}.\nTo create conditions in queries, use the static fields in the class {@link $TQueryFields}.\n", typeNameOfDataClass, typeNameOfDataClass);
|
||||
|
||||
createStoreFactoryField(builder);
|
||||
createConstructor(builder);
|
||||
|
||||
List<ParentSpec> parents = determineParentSpecs(dataClassTypeElement);
|
||||
if (parents.isEmpty()) {
|
||||
createGetterForDataTypeWithoutParent(builder, typeNameOfDataClass);
|
||||
} else {
|
||||
createOverallGetterForDataTypeWithParents(builder, typeNameOfDataClass);
|
||||
createMutableGetterForDataTypeWithParents(typeNameOfDataClass, parents, builder);
|
||||
createPartialGetterForDataTypeWithParents(builder, typeNameOfDataClass, parents);
|
||||
}
|
||||
|
||||
JavaFile.builder(packageName, builder.build())
|
||||
.build()
|
||||
.writeTo(processingEnv.getFiler());
|
||||
}
|
||||
|
||||
private void createMutableGetterForDataTypeWithParents(TypeName typeNameOfDataClass, List<ParentSpec> parents, TypeSpec.Builder builder) {
|
||||
builder.addMethod(
|
||||
createGetMutableMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to modify elements of the type {@link $T}.\nTo do so, an id has to be specified for each parent type.\n",
|
||||
parents,
|
||||
ParentSpec::buildParameterNameWithIdSuffix,
|
||||
ParentSpec::appendParentAsIdStringArgument
|
||||
)
|
||||
);
|
||||
|
||||
if (isEveryParentModelObject(parents)) {
|
||||
builder.addMethod(
|
||||
createGetMutableMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to modify elements of the type {@link $T}.\nTo do so, an instance of each parent type has to be specified.\n",
|
||||
parents,
|
||||
ParentSpec::buildIdGetterWithParameterName,
|
||||
ParentSpec::appendParentAsObjectArgument
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private MethodSpec createGetMutableMethodSpec(TypeName typeNameOfDataClass,
|
||||
String javaDoc,
|
||||
List<ParentSpec> parents,
|
||||
Function<ParentSpec, String> parentIdProcessor,
|
||||
BiConsumer<MethodSpec.Builder, ParentSpec> parentArgumentProcessor) {
|
||||
MethodSpec.Builder getMutableBuilder = MethodSpec
|
||||
.methodBuilder("getMutable")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addJavadoc(javaDoc, typeNameOfDataClass)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_MUTABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement(
|
||||
"return storeFactory.getMutable($T.class, $L)",
|
||||
typeNameOfDataClass,
|
||||
parents.stream().map(parentIdProcessor).reduce((s1, s2) -> s1 + ", " + s2).orElseThrow()
|
||||
);
|
||||
|
||||
parents.forEach(
|
||||
parent -> parentArgumentProcessor.accept(getMutableBuilder, parent)
|
||||
);
|
||||
|
||||
return getMutableBuilder.build();
|
||||
}
|
||||
|
||||
private boolean isEveryParentModelObject(List<ParentSpec> parents) {
|
||||
return parents.stream().allMatch(ParentSpec::isModelObject);
|
||||
}
|
||||
|
||||
private void createPartialGetterForDataTypeWithParents(TypeSpec.Builder builder, TypeName typeNameOfDataClass, List<ParentSpec> parents) {
|
||||
for (int i = 0; i < parents.size(); i++) {
|
||||
String javaDocParentDescriptor;
|
||||
if (i == 0) {
|
||||
javaDocParentDescriptor = "only to the first parent";
|
||||
} else if (i < parents.size() - 1) {
|
||||
javaDocParentDescriptor = "only to the first " + (i + 1) + "parents";
|
||||
} else {
|
||||
javaDocParentDescriptor = "to all parents";
|
||||
}
|
||||
|
||||
int currentParentLimit = i + 1;
|
||||
builder.addMethod(
|
||||
createPartialGetterMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to query elements of the type {@link $T} limited " + javaDocParentDescriptor + " specified by their ids.\n",
|
||||
parents,
|
||||
currentParentLimit,
|
||||
ParentSpec::buildParameterNameWithIdSuffix,
|
||||
ParentSpec::appendParentAsIdStringArgument
|
||||
)
|
||||
);
|
||||
|
||||
if (isEveryParentModelObject(parents)) {
|
||||
builder.addMethod(
|
||||
createPartialGetterMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to query elements of the type {@link $T} limited " + javaDocParentDescriptor + " specified as instances of the parent type.\n",
|
||||
parents,
|
||||
currentParentLimit,
|
||||
ParentSpec::buildIdGetterWithParameterName,
|
||||
ParentSpec::appendParentAsObjectArgument
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MethodSpec createPartialGetterMethodSpec(TypeName typeNameOfDataClass,
|
||||
String javaDoc,
|
||||
List<ParentSpec> parents,
|
||||
int parentLimit,
|
||||
Function<ParentSpec, String> parentIdProcessor,
|
||||
BiConsumer<MethodSpec.Builder, ParentSpec> parentArgumentProcessor) {
|
||||
MethodSpec.Builder getBuilder = MethodSpec
|
||||
.methodBuilder(parentLimit == parents.size() ? "get" : "getOverlapping")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addJavadoc(javaDoc, typeNameOfDataClass)
|
||||
.addStatement(
|
||||
"return storeFactory.getReadOnly($T.class, $L)",
|
||||
typeNameOfDataClass,
|
||||
parents.stream()
|
||||
.limit(parentLimit)
|
||||
.map(parentIdProcessor)
|
||||
.reduce((s1, s2) -> s1 + ", " + s2)
|
||||
.orElseThrow()
|
||||
);
|
||||
|
||||
parents.stream()
|
||||
.limit(parentLimit)
|
||||
.forEach(parent -> parentArgumentProcessor.accept(getBuilder, parent));
|
||||
|
||||
return getBuilder.build();
|
||||
}
|
||||
|
||||
private void createOverallGetterForDataTypeWithParents(TypeSpec.Builder builder, TypeName typeNameOfDataClass) {
|
||||
builder.addMethod(
|
||||
MethodSpec.methodBuilder("getOverall")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement("return storeFactory.getReadOnly($T.class)", typeNameOfDataClass)
|
||||
.addJavadoc("Returns a store to overall query elements of the type {@link $T} independent of any parent.\n", typeNameOfDataClass)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void createGetterForDataTypeWithoutParent(TypeSpec.Builder builder, TypeName typeNameOfDataClass) {
|
||||
builder.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement("return storeFactory.getReadOnly($T.class)", typeNameOfDataClass)
|
||||
.addJavadoc("Returns a store to query elements of the type {@link $T}.\n", typeNameOfDataClass)
|
||||
.build());
|
||||
builder.addMethod(
|
||||
MethodSpec.methodBuilder("getMutable")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_MUTABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement("return storeFactory.getMutable($T.class)", typeNameOfDataClass)
|
||||
.addJavadoc("Returns a store to modify elements of the type {@link $T}.\n", typeNameOfDataClass)
|
||||
.build());
|
||||
}
|
||||
|
||||
private List<ParentSpec> determineParentSpecs(TypeElement typeElement) {
|
||||
return new QueryableTypeParentProcessor().getQueryableTypeValues(typeElement)
|
||||
.stream()
|
||||
.map(queryableType -> {
|
||||
String parentClassPackage = queryableType.substring(0, queryableType.lastIndexOf("."));
|
||||
String parentClassName = queryableType.substring(queryableType.lastIndexOf(".") + 1);
|
||||
String parameterName = lowercaseFirstLetter(parentClassName);
|
||||
return new ParentSpec(parentClassPackage, parentClassName, parameterName, isParentModelObject(queryableType));
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean isParentModelObject(String parentType) {
|
||||
try {
|
||||
Class<?> parentClass = Class.forName(parentType);
|
||||
return Arrays.stream(parentClass.getInterfaces()).anyMatch(parentInterface -> parentInterface.getName().equals(ModelObject.class.getName()));
|
||||
} catch (ClassNotFoundException e) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, String.format("Failed to find class of parent '%s'. Unable to determine whether this is a ModelObject or not. Will not generate factory methods for parent objects, only for ids.", parentType));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String lowercaseFirstLetter(String parentClassName) {
|
||||
return parentClassName.substring(0, 1).toLowerCase(Locale.ENGLISH) + parentClassName.substring(1);
|
||||
}
|
||||
|
||||
private void createConstructor(TypeSpec.Builder builder) {
|
||||
builder.addMethod(
|
||||
MethodSpec
|
||||
.constructorBuilder()
|
||||
.addParameter(QueryableStoreFactory.class, "storeFactory")
|
||||
.addStatement("this.storeFactory = storeFactory")
|
||||
.addAnnotation(Inject.class)
|
||||
.addJavadoc("Instances should not be created manually, but injected by dependency injection using {@link $T}.\n", Inject.class)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void createStoreFactoryField(TypeSpec.Builder builder) {
|
||||
builder.addField(QueryableStoreFactory.class, "storeFactory", Modifier.PRIVATE, Modifier.FINAL);
|
||||
}
|
||||
|
||||
private record ParentSpec(String classPackage, String className, String parameterName, boolean isModelObject) {
|
||||
String buildParameterNameWithIdSuffix() {
|
||||
return parameterName + "Id";
|
||||
}
|
||||
|
||||
String buildIdGetterWithParameterName() {
|
||||
return parameterName + ".getId()";
|
||||
}
|
||||
|
||||
static void appendParentAsIdStringArgument(MethodSpec.Builder builder, ParentSpec parent) {
|
||||
builder.addParameter(String.class, parent.buildParameterNameWithIdSuffix());
|
||||
}
|
||||
|
||||
static void appendParentAsObjectArgument(MethodSpec.Builder builder, ParentSpec parent) {
|
||||
builder.addParameter(ClassName.get(parent.classPackage(), parent.className()), parent.parameterName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
interface FieldInitializer {
|
||||
void initialize(FieldSpec.Builder fieldBuilder, TypeElement element, String fieldClass, String fieldName);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
|
||||
class NumberQueryFieldHandler extends QueryFieldHandler {
|
||||
public NumberQueryFieldHandler(String packageName, String className) {
|
||||
this(packageName, className, null);
|
||||
}
|
||||
|
||||
public NumberQueryFieldHandler(String packageName, String className, String suffix) {
|
||||
super(
|
||||
"NumberQueryField",
|
||||
new TypeName[]{ClassName.get(packageName, className)},
|
||||
(fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
|
||||
.initializer(
|
||||
"new $T<>($S)",
|
||||
ClassName.get("sonia.scm.store", "QueryableStore").nestedClass(fieldClass),
|
||||
fieldName
|
||||
),
|
||||
suffix
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.google.auto.common.MoreElements;
|
||||
import com.google.auto.common.MoreTypes;
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
import com.squareup.javapoet.JavaFile;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
class QueryFieldClassCreator {
|
||||
|
||||
private static final String SIZE_SUFFIX = "SIZE";
|
||||
private static final String STORE_PACKAGE_NAME = "sonia.scm.store";
|
||||
private static final String QUERYABLE_STORE_CLASS_NAME = "QueryableStore";
|
||||
private static final String ID_QUERY_FIELD_CLASS_NAME = "IdQueryField";
|
||||
|
||||
private static final FieldInitializer SIMPLE_INITIALIZER = (fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
|
||||
.initializer(
|
||||
"new $T<>($S)",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(fieldClass),
|
||||
fieldName
|
||||
);
|
||||
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
|
||||
QueryFieldClassCreator(ProcessingEnvironment processingEnv) {
|
||||
this.processingEnv = processingEnv;
|
||||
}
|
||||
|
||||
void createQueryFieldClass(Element element, String packageName, TypeElement typeElement) throws IOException {
|
||||
TypeSpec.Builder builder =
|
||||
TypeSpec
|
||||
.classBuilder(element.getSimpleName() + "QueryFields")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
|
||||
.addJavadoc("Generated query fields for type {@link $T}.\nTo create a queryable store for this, use an injected instance of the {@link $TStoreFactory}.\n", TypeName.get(typeElement.asType()), TypeName.get(typeElement.asType()));
|
||||
|
||||
createPrivateConstructor(builder);
|
||||
processParents(typeElement, builder);
|
||||
processId(typeElement, builder);
|
||||
processFields(typeElement, builder);
|
||||
|
||||
JavaFile.builder(packageName, builder.build())
|
||||
.build()
|
||||
.writeTo(processingEnv.getFiler());
|
||||
}
|
||||
|
||||
private void createPrivateConstructor(TypeSpec.Builder builder) {
|
||||
builder.addMethod(
|
||||
MethodSpec
|
||||
.constructorBuilder()
|
||||
.addModifiers(Modifier.PRIVATE)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void processParents(TypeElement typeElement, TypeSpec.Builder builder) {
|
||||
new QueryableTypeParentProcessor().getQueryableTypeValues(typeElement)
|
||||
.forEach(queryableType -> {
|
||||
String parentClassPackage = queryableType.substring(0, queryableType.lastIndexOf("."));
|
||||
String parentClassName = queryableType.substring(queryableType.lastIndexOf(".") + 1);
|
||||
builder.addField(
|
||||
FieldSpec
|
||||
.builder(
|
||||
ParameterizedTypeName.get(
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME)
|
||||
.nestedClass(ID_QUERY_FIELD_CLASS_NAME),
|
||||
TypeName.get(typeElement.asType())),
|
||||
parentClassName.toUpperCase(Locale.ENGLISH) + "_ID"
|
||||
)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
|
||||
.initializer(
|
||||
"new $T<>($T.class)",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(ID_QUERY_FIELD_CLASS_NAME),
|
||||
ClassName.get(parentClassPackage, parentClassName))
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
private void processId(TypeElement typeElement, TypeSpec.Builder builder) {
|
||||
builder.addField(
|
||||
FieldSpec
|
||||
.builder(
|
||||
ParameterizedTypeName.get(
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME)
|
||||
.nestedClass(ID_QUERY_FIELD_CLASS_NAME),
|
||||
TypeName.get(typeElement.asType())),
|
||||
"INTERNAL_ID"
|
||||
)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
|
||||
.initializer(
|
||||
"new $T<>()",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(ID_QUERY_FIELD_CLASS_NAME))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void processFields(TypeElement typeElement, TypeSpec.Builder builder) {
|
||||
processFields(typeElement, typeElement, builder);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void processFields(TypeElement typeElement, TypeElement superTypeElement, TypeSpec.Builder builder) {
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getAllMembers(typeElement)
|
||||
.stream()
|
||||
.filter(member -> member.getKind() == ElementKind.FIELD)
|
||||
.filter(member -> !member.getModifiers().contains(Modifier.STATIC))
|
||||
.filter(member -> !member.getModifiers().contains(Modifier.TRANSIENT))
|
||||
.flatMap(field ->
|
||||
createFieldSpec(
|
||||
superTypeElement,
|
||||
MoreElements.asVariable(field))
|
||||
)
|
||||
.forEach(builder::addField);
|
||||
TypeElement superclass = (TypeElement) processingEnv.getTypeUtils().asElement(typeElement.getSuperclass());
|
||||
if (superclass != null && !superclass.getQualifiedName().toString().equals(Object.class.getCanonicalName())) {
|
||||
processFields(superclass, typeElement, builder);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<FieldSpec> createFieldSpec(TypeElement element, VariableElement field) {
|
||||
TypeMirror effectiveFieldType = determineFieldType(field);
|
||||
return createFieldHandler(effectiveFieldType).stream()
|
||||
.map(queryFieldHandler -> {
|
||||
String fieldName = field.getSimpleName().toString();
|
||||
String fieldClass = queryFieldHandler.getClazz();
|
||||
TypeName[] furtherGenerics = queryFieldHandler.getGenerics();
|
||||
TypeName[] generics = new TypeName[furtherGenerics.length + 1];
|
||||
generics[0] = TypeName.get(element.asType());
|
||||
System.arraycopy(furtherGenerics, 0, generics, 1, furtherGenerics.length);
|
||||
FieldSpec.Builder fieldBuilder = FieldSpec
|
||||
.builder(
|
||||
ParameterizedTypeName.get(
|
||||
ClassName
|
||||
.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME)
|
||||
.nestedClass(fieldClass),
|
||||
generics),
|
||||
determineFieldNameWithSuffix(fieldName, queryFieldHandler).toUpperCase(Locale.ENGLISH)
|
||||
)
|
||||
.addJavadoc("Generated query field to create conditions for field {@link $L#$L} of type {@link $L}.\n", TypeName.get(element.asType()), fieldName, effectiveFieldType)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
|
||||
queryFieldHandler.getInitializer().initialize(fieldBuilder, element, fieldClass, fieldName);
|
||||
return fieldBuilder.build();
|
||||
});
|
||||
}
|
||||
|
||||
private TypeMirror determineFieldType(VariableElement field) {
|
||||
return new AnnotationProcessor().findAnnotation(field, XmlJavaTypeAdapter.class)
|
||||
.map(this::determineTypeFromAdapter)
|
||||
.orElseGet(field::asType);
|
||||
}
|
||||
|
||||
private TypeMirror determineTypeFromAdapter(AnnotationMirror annotationMirror) {
|
||||
AnnotationValue value = new AnnotationHelper().findAnnotationValue(annotationMirror, "value").orElseThrow();
|
||||
TypeMirror adapterType = (TypeMirror) value.getValue();
|
||||
TypeMirror xmlAdapterType = processingEnv.getTypeUtils()
|
||||
.directSupertypes(adapterType)
|
||||
.stream()
|
||||
.filter(typeMirror -> processingEnv.getTypeUtils()
|
||||
.isAssignable(
|
||||
processingEnv.getTypeUtils().erasure(typeMirror),
|
||||
processingEnv.getElementUtils().getTypeElement(XmlAdapter.class.getCanonicalName()).asType()
|
||||
))
|
||||
.findFirst()
|
||||
.orElseThrow(RuntimeException::new);
|
||||
DeclaredType declaredType = MoreTypes.asDeclared(xmlAdapterType);
|
||||
return declaredType.getTypeArguments().get(0);
|
||||
}
|
||||
|
||||
private Collection<QueryFieldHandler> createFieldHandler(TypeMirror fieldType) {
|
||||
TypeMirror collectionType = processingEnv.getElementUtils().getTypeElement(Collection.class.getCanonicalName()).asType();
|
||||
TypeMirror erasure = processingEnv.getTypeUtils().erasure(fieldType);
|
||||
if (processingEnv.getTypeUtils().isAssignable(erasure, collectionType)) {
|
||||
return List.of(
|
||||
new QueryFieldHandler(
|
||||
"CollectionQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER
|
||||
),
|
||||
new QueryFieldHandler(
|
||||
"CollectionSizeQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER,
|
||||
SIZE_SUFFIX
|
||||
)
|
||||
);
|
||||
}
|
||||
TypeMirror mapType = processingEnv.getElementUtils().getTypeElement(Map.class.getCanonicalName()).asType();
|
||||
if (processingEnv.getTypeUtils().isAssignable(erasure, mapType)) {
|
||||
return List.of(
|
||||
new QueryFieldHandler(
|
||||
"MapQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER
|
||||
),
|
||||
new QueryFieldHandler(
|
||||
"MapSizeQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER,
|
||||
SIZE_SUFFIX
|
||||
)
|
||||
);
|
||||
}
|
||||
Element fieldAsElement = processingEnv.getTypeUtils().asElement(fieldType);
|
||||
if (fieldAsElement != null && fieldAsElement.getKind() == ElementKind.ENUM) {
|
||||
return List.of(new QueryFieldHandler(
|
||||
"EnumQueryField",
|
||||
new TypeName[]{TypeName.get(fieldAsElement.asType())},
|
||||
(fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
|
||||
.initializer(
|
||||
"new $T<>($S)",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(fieldClass),
|
||||
fieldName
|
||||
)
|
||||
));
|
||||
}
|
||||
return switch (fieldType.toString()) {
|
||||
case "java.lang.String" -> List.of(
|
||||
new QueryFieldHandler(
|
||||
"StringQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER));
|
||||
case "boolean", "java.lang.Boolean" -> List.of(
|
||||
new QueryFieldHandler(
|
||||
"BooleanQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER));
|
||||
case "int", "java.lang.Integer" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Integer"));
|
||||
case "long", "java.lang.Long" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Long"));
|
||||
case "float", "java.lang.Float" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Float"));
|
||||
case "double", "java.lang.Double" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Double"));
|
||||
case "java.util.Date", "java.time.Instant" -> List.of(
|
||||
new QueryFieldHandler(
|
||||
"InstantQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER));
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
private String determineFieldNameWithSuffix(String fieldName, QueryFieldHandler fieldHandler) {
|
||||
return fieldHandler.getSuffix()
|
||||
.map(suffix -> String.format("%s_%s", fieldName, suffix))
|
||||
.orElse(fieldName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
class QueryFieldHandler {
|
||||
private final String clazz;
|
||||
private final TypeName[] generics;
|
||||
private final FieldInitializer initializer;
|
||||
private final String suffix;
|
||||
|
||||
public QueryFieldHandler(String clazz, TypeName[] generics, FieldInitializer initializer) {
|
||||
this(clazz, generics, initializer, null);
|
||||
}
|
||||
|
||||
public Optional<String> getSuffix() {
|
||||
return Optional.ofNullable(suffix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.google.auto.common.MoreElements;
|
||||
import org.kohsuke.MetaInfServices;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.Processor;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.annotation.processing.SupportedSourceVersion;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
@SupportedAnnotationTypes("sonia.scm.store.QueryableType")
|
||||
@MetaInfServices(Processor.class)
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_17)
|
||||
public class QueryableTypeAnnotationProcessor extends AbstractProcessor {
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
|
||||
for (TypeElement annotation : annotations) {
|
||||
log("Found annotation: " + annotation.getQualifiedName());
|
||||
roundEnvironment.getElementsAnnotatedWith(annotation).forEach(element -> {
|
||||
log("Found annotated element: " + element.getSimpleName());
|
||||
tryToCreateQueryFieldClass(element);
|
||||
tryToCreateFactoryClass(element);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void tryToCreateQueryFieldClass(Element element) {
|
||||
TypeElement typeElement = MoreElements.asType(element);
|
||||
getPackageName(typeElement)
|
||||
.ifPresent(packageName -> {
|
||||
try {
|
||||
new QueryFieldClassCreator(processingEnv).createQueryFieldClass(element, packageName, typeElement);
|
||||
} catch (IOException e) {
|
||||
error("Failed to create query field class for type " + typeElement + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void tryToCreateFactoryClass(Element element) {
|
||||
TypeElement typeElement = MoreElements.asType(element);
|
||||
getPackageName(typeElement)
|
||||
.ifPresent(packageName -> {
|
||||
try {
|
||||
new FactoryClassCreator(processingEnv).createFactoryClass(element, packageName, typeElement);
|
||||
} catch (IOException e) {
|
||||
error("Failed to create factory class for type " + typeElement + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private Optional<String> getPackageName(TypeElement typeElement) {
|
||||
Element enclosingElement = typeElement.getEnclosingElement();
|
||||
try {
|
||||
return of(MoreElements.asPackage(enclosingElement).getQualifiedName().toString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
error("Could not determine package name for " + typeElement + ". QueryableType annotation does not support inner classes. Exception: " + e.getMessage());
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String message) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
|
||||
}
|
||||
|
||||
private void error(String message) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
class QueryableTypeParentProcessor {
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getQueryableTypeValues(TypeElement typeElement) {
|
||||
return new AnnotationProcessor().findAnnotation(typeElement, QueryableType.class)
|
||||
.map(annotationMirror -> {
|
||||
Optional<? extends AnnotationValue> value = new AnnotationHelper().findAnnotationValue(annotationMirror, "value");
|
||||
if (value.isEmpty()) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
List<AnnotationValue> parentClassTypes = (List<AnnotationValue>) value.orElseThrow().getValue();
|
||||
return parentClassTypes.stream()
|
||||
.map(AnnotationValue::getValue)
|
||||
.map(Object::toString)
|
||||
.toList();
|
||||
})
|
||||
.orElseGet(List::of);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
sonia.scm.annotation.QueryableTypeAnnotationProcessor,isolating
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.google.common.truth.Truth;
|
||||
import com.google.testing.compile.JavaFileObjects;
|
||||
import com.google.testing.compile.JavaSourcesSubjectFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import javax.tools.JavaFileObject;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("java:S115") // we do not heed enum naming conventions for better readability in the test
|
||||
class QueryableTypeAnnotationProcessorTest {
|
||||
|
||||
enum FieldScenario {
|
||||
A("empty class"),
|
||||
B("string query field"),
|
||||
C("boolean query fields"),
|
||||
D("number query fields"),
|
||||
E("enum query field"),
|
||||
F("collection query field"),
|
||||
G("map query field"),
|
||||
H("unknown field"),
|
||||
I("instant field mapped to string"),
|
||||
K("parent id field"),
|
||||
L("unmapped instant field"),
|
||||
M("unmapped java util date field"),
|
||||
N("static field"),
|
||||
O("transient field"),
|
||||
BSub("fields from super class");
|
||||
|
||||
private final String description;
|
||||
|
||||
FieldScenario(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "should test field scenario for {0}")
|
||||
@EnumSource(FieldScenario.class)
|
||||
void shouldTest(FieldScenario scenario) {
|
||||
JavaFileObject someObject = JavaFileObjects.forResource(String.format("sonia/scm/testing/%s.java", scenario.name()));
|
||||
Truth.assert_()
|
||||
.about(JavaSourcesSubjectFactory.javaSources())
|
||||
.that(List.of(someObject))
|
||||
.processedWith(new QueryableTypeAnnotationProcessor())
|
||||
.compilesWithoutError()
|
||||
.and()
|
||||
.generatesSources(JavaFileObjects.forResource(String.format("sonia/scm/testing/%sQueryFields.java", scenario.name())));
|
||||
}
|
||||
|
||||
enum FactoryScenario {
|
||||
A("class without parent"),
|
||||
OneParent("class with one parent"),
|
||||
TwoParents("class with two parents"),
|
||||
ThreeParents("class with three parents"),
|
||||
OneNonModelObjectParent("class with one model object parent and one non model object parent");
|
||||
|
||||
private final String description;
|
||||
|
||||
FactoryScenario(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "should test factory scenario for {0}")
|
||||
@EnumSource(FactoryScenario.class)
|
||||
void shouldTest(FactoryScenario scenario) {
|
||||
JavaFileObject someObject = JavaFileObjects.forResource(String.format("sonia/scm/testing/%s.java", scenario.name()));
|
||||
Truth.assert_()
|
||||
.about(JavaSourcesSubjectFactory.javaSources())
|
||||
.that(List.of(someObject))
|
||||
.processedWith(new QueryableTypeAnnotationProcessor())
|
||||
.compilesWithoutError()
|
||||
.and()
|
||||
.generatesSources(JavaFileObjects.forResource(String.format("sonia/scm/testing/%sStoreFactory.java", scenario.name())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleInnerClasses() {
|
||||
JavaFileObject someObject = JavaFileObjects.forResource("sonia/scm/testing/InnerA.java");
|
||||
Truth.assert_()
|
||||
.about(JavaSourcesSubjectFactory.javaSources())
|
||||
.that(List.of(someObject))
|
||||
.processedWith(new QueryableTypeAnnotationProcessor())
|
||||
.failsToCompile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class A {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class AQueryFields {
|
||||
public static final QueryableStore.IdQueryField<A> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
private AQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
|
||||
public class AStoreFactory {
|
||||
|
||||
private final QueryableStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
AStoreFactory(QueryableStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public QueryableStore<A> get() {
|
||||
return storeFactory.getReadOnly(A.class);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<A> getMutable() {
|
||||
return storeFactory.getMutable(A.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class B {
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class BQueryFields {
|
||||
public static final QueryableStore.IdQueryField<B> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.StringQueryField<B> NAME =
|
||||
new QueryableStore.StringQueryField<>("name");
|
||||
|
||||
private BQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class BSub extends B {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class BSubQueryFields {
|
||||
public static final QueryableStore.IdQueryField<BSub> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.StringQueryField<BSub> NAME =
|
||||
new QueryableStore.StringQueryField<>("name");
|
||||
|
||||
private BSubQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class C {
|
||||
private boolean active;
|
||||
private Boolean enabled;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class CQueryFields {
|
||||
public static final QueryableStore.IdQueryField<C> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.BooleanQueryField<C> ACTIVE =
|
||||
new QueryableStore.BooleanQueryField<>("active");
|
||||
|
||||
public static final QueryableStore.BooleanQueryField<C> ENABLED =
|
||||
new QueryableStore.BooleanQueryField<>("enabled");
|
||||
|
||||
private CQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class D {
|
||||
private int age;
|
||||
private Integer weight;
|
||||
|
||||
private long creationTime;
|
||||
private Long lastModified;
|
||||
|
||||
private float height;
|
||||
private Float width;
|
||||
|
||||
private double price;
|
||||
private Double margin;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import java.lang.Double;
|
||||
import java.lang.Float;
|
||||
import java.lang.Integer;
|
||||
import java.lang.Long;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class DQueryFields {
|
||||
public static final QueryableStore.IdQueryField<D> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.NumberQueryField<D, Integer> AGE =
|
||||
new QueryableStore.NumberQueryField<>("age");
|
||||
public static final QueryableStore.NumberQueryField<D, Integer> WEIGHT =
|
||||
new QueryableStore.NumberQueryField<>("weight");
|
||||
|
||||
public static final QueryableStore.NumberQueryField<D, Long> CREATIONTIME =
|
||||
new QueryableStore.NumberQueryField<>("creationTime");
|
||||
public static final QueryableStore.NumberQueryField<D, Long> LASTMODIFIED =
|
||||
new QueryableStore.NumberQueryField<>("lastModified");
|
||||
|
||||
public static final QueryableStore.NumberQueryField<D, Float> HEIGHT =
|
||||
new QueryableStore.NumberQueryField<>("height");
|
||||
public static final QueryableStore.NumberQueryField<D, Float> WIDTH =
|
||||
new QueryableStore.NumberQueryField<>("width");
|
||||
|
||||
public static final QueryableStore.NumberQueryField<D, Double> PRICE =
|
||||
new QueryableStore.NumberQueryField<>("price");
|
||||
public static final QueryableStore.NumberQueryField<D, Double> MARGIN =
|
||||
new QueryableStore.NumberQueryField<>("margin");
|
||||
|
||||
private DQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class E {
|
||||
private Stage stage;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class EQueryFields {
|
||||
public static final QueryableStore.IdQueryField<E> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.EnumQueryField<E, Stage> STAGE =
|
||||
new QueryableStore.EnumQueryField<>("stage");
|
||||
|
||||
private EQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@QueryableType
|
||||
public class F {
|
||||
private List<String> names;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class FQueryFields {
|
||||
public static final QueryableStore.IdQueryField<F> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.CollectionQueryField<F> NAMES =
|
||||
new QueryableStore.CollectionQueryField<>("names");
|
||||
|
||||
public static final QueryableStore.CollectionSizeQueryField<F> NAMES_SIZE =
|
||||
new QueryableStore.CollectionSizeQueryField<>("names");
|
||||
|
||||
private FQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
import java.util.Map;
|
||||
|
||||
@QueryableType
|
||||
public class G {
|
||||
private Map<String, Object> dictionary;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class GQueryFields {
|
||||
public static final QueryableStore.IdQueryField<G> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.MapQueryField<G> DICTIONARY =
|
||||
new QueryableStore.MapQueryField<>("dictionary");
|
||||
|
||||
public static final QueryableStore.MapSizeQueryField<G> DICTIONARY_SIZE =
|
||||
new QueryableStore.MapSizeQueryField<>("dictionary");
|
||||
|
||||
private GQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
import java.util.Map;
|
||||
|
||||
@QueryableType
|
||||
public class H {
|
||||
private Object somethingStrange;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class HQueryFields {
|
||||
public static final QueryableStore.IdQueryField<H> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
private HQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import sonia.scm.store.QueryableType;
|
||||
import sonia.scm.xml.XmlInstantAdapter;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@QueryableType
|
||||
public class I {
|
||||
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
||||
private Instant birthday;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class IQueryFields {
|
||||
public static final QueryableStore.IdQueryField<I> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.StringQueryField<I> BIRTHDAY =
|
||||
new QueryableStore.StringQueryField<>("birthday");
|
||||
|
||||
private IQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
public class InnerA {
|
||||
|
||||
|
||||
|
||||
@QueryableType
|
||||
public static class A {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType(Repository.class)
|
||||
public class K {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class KQueryFields {
|
||||
public static final QueryableStore.IdQueryField<K> REPOSITORY_ID =
|
||||
new QueryableStore.IdQueryField<>(Repository.class);
|
||||
|
||||
public static final QueryableStore.IdQueryField<K> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
private KQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import sonia.scm.store.QueryableType;
|
||||
import sonia.scm.xml.XmlInstantAdapter;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@QueryableType
|
||||
public class L {
|
||||
private Instant birthday;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class LQueryFields {
|
||||
public static final QueryableStore.IdQueryField<L> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.InstantQueryField<L> BIRTHDAY =
|
||||
new QueryableStore.InstantQueryField<>("birthday");
|
||||
|
||||
private LQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@QueryableType
|
||||
public class M {
|
||||
private Date birthday;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class MQueryFields {
|
||||
public static final QueryableStore.IdQueryField<M> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
public static final QueryableStore.InstantQueryField<M> BIRTHDAY =
|
||||
new QueryableStore.InstantQueryField<>("birthday");
|
||||
|
||||
private MQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class N {
|
||||
private static String someField;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class NQueryFields {
|
||||
public static final QueryableStore.IdQueryField<N> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
private NQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType
|
||||
public class O {
|
||||
private transient String transientField;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.store.QueryableStore;
|
||||
|
||||
public final class OQueryFields {
|
||||
public static final QueryableStore.IdQueryField<O> INTERNAL_ID =
|
||||
new QueryableStore.IdQueryField<>();
|
||||
|
||||
private OQueryFields() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType({NamespaceAndName.class, Repository.class})
|
||||
public class OneNonModelObjectParent {
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.lang.String;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
|
||||
public class OneNonModelObjectParentStoreFactory {
|
||||
|
||||
private final QueryableStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
OneNonModelObjectParentStoreFactory(QueryableStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public QueryableStore<OneNonModelObjectParent> getOverall() {
|
||||
return storeFactory.getReadOnly(OneNonModelObjectParent.class);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<OneNonModelObjectParent> getMutable(String namespaceAndNameId, String repositoryId) {
|
||||
return storeFactory.getMutable(OneNonModelObjectParent.class, namespaceAndNameId, repositoryId);
|
||||
}
|
||||
|
||||
public QueryableStore<OneNonModelObjectParent> getOverlapping(String namespaceAndNameId) {
|
||||
return storeFactory.getReadOnly(OneNonModelObjectParent.class, namespaceAndNameId);
|
||||
}
|
||||
|
||||
public QueryableStore<OneNonModelObjectParent> get(String namespaceAndNameId, String repositoryId) {
|
||||
return storeFactory.getReadOnly(OneNonModelObjectParent.class, namespaceAndNameId, repositoryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType(Repository.class)
|
||||
public class OneParent {
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.lang.String;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
|
||||
public class OneParentStoreFactory {
|
||||
|
||||
private final QueryableStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
OneParentStoreFactory(QueryableStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public QueryableStore<OneParent> getOverall() {
|
||||
return storeFactory.getReadOnly(OneParent.class);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<OneParent> getMutable(String repositoryId) {
|
||||
return storeFactory.getMutable(OneParent.class, repositoryId);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<OneParent> getMutable(Repository repository) {
|
||||
return storeFactory.getMutable(OneParent.class, repository.getId());
|
||||
}
|
||||
|
||||
public QueryableStore<OneParent> get(String repositoryId) {
|
||||
return storeFactory.getReadOnly(OneParent.class, repositoryId);
|
||||
}
|
||||
|
||||
public QueryableStore<OneParent> get(Repository repository) {
|
||||
return storeFactory.getReadOnly(OneParent.class, repository.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType({Repository.class, User.class, Group.class})
|
||||
public class ThreeParents {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.lang.String;
|
||||
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
public class ThreeParentsStoreFactory {
|
||||
|
||||
private final QueryableStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
ThreeParentsStoreFactory(QueryableStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> getOverall() {
|
||||
return storeFactory.getReadOnly(ThreeParents.class);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<ThreeParents> getMutable(String repositoryId, String userId, String groupId) {
|
||||
return storeFactory.getMutable(ThreeParents.class, repositoryId, userId, groupId);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<ThreeParents> getMutable(Repository repository, User user, Group group) {
|
||||
return storeFactory.getMutable(ThreeParents.class, repository.getId(), user.getId(), group.getId());
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> getOverlapping(String repositoryId) {
|
||||
return storeFactory.getReadOnly(ThreeParents.class, repositoryId);
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> getOverlapping(Repository repository) {
|
||||
return storeFactory.getReadOnly(ThreeParents.class, repository.getId());
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> getOverlapping(String repositoryId, String userId) {
|
||||
return storeFactory.getReadOnly(ThreeParents.class, repositoryId, userId);
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> getOverlapping(Repository repository, User user) {
|
||||
return storeFactory.getReadOnly(ThreeParents.class, repository.getId(), user.getId());
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> get(String repositoryId, String userId, String groupId) {
|
||||
return storeFactory.getReadOnly(ThreeParents.class, repositoryId, userId, groupId);
|
||||
}
|
||||
|
||||
public QueryableStore<ThreeParents> get(Repository repository, User user, Group group) {
|
||||
return storeFactory.getReadOnly(ThreeParents.class, repository.getId(), user.getId(), group.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
@QueryableType({Repository.class, User.class})
|
||||
public class TwoParents {
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.testing;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import java.lang.String;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
public class TwoParentsStoreFactory {
|
||||
|
||||
private final QueryableStoreFactory storeFactory;
|
||||
|
||||
@Inject
|
||||
TwoParentsStoreFactory(QueryableStoreFactory storeFactory) {
|
||||
this.storeFactory = storeFactory;
|
||||
}
|
||||
|
||||
public QueryableStore<TwoParents> getOverall() {
|
||||
return storeFactory.getReadOnly(TwoParents.class);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<TwoParents> getMutable(String repositoryId, String userId) {
|
||||
return storeFactory.getMutable(TwoParents.class, repositoryId, userId);
|
||||
}
|
||||
|
||||
public QueryableMutableStore<TwoParents> getMutable(Repository repository, User user) {
|
||||
return storeFactory.getMutable(TwoParents.class, repository.getId(), user.getId());
|
||||
}
|
||||
|
||||
public QueryableStore<TwoParents> getOverlapping(String repositoryId) {
|
||||
return storeFactory.getReadOnly(TwoParents.class, repositoryId);
|
||||
}
|
||||
|
||||
public QueryableStore<TwoParents> getOverlapping(Repository repository) {
|
||||
return storeFactory.getReadOnly(TwoParents.class, repository.getId());
|
||||
}
|
||||
|
||||
public QueryableStore<TwoParents> get(String repositoryId, String userId) {
|
||||
return storeFactory.getReadOnly(TwoParents.class, repositoryId, userId);
|
||||
}
|
||||
|
||||
public QueryableStore<TwoParents> get(Repository repository, User user) {
|
||||
return storeFactory.getReadOnly(TwoParents.class, repository.getId(), user.getId());
|
||||
}
|
||||
}
|
||||
@@ -77,4 +77,12 @@ public interface ExtensionProcessor
|
||||
default Iterable<Class<?>> getIndexedTypes() {
|
||||
return emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all queryable types.
|
||||
* @since 3.7.0
|
||||
*/
|
||||
default Iterable<QueryableTypeDescriptor> getQueryableTypes() {
|
||||
return emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import lombok.ToString;
|
||||
import java.util.HashSet;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@NoArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlAccessType;
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType;
|
||||
import jakarta.xml.bind.annotation.XmlElement;
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import lombok.*;
|
||||
import sonia.scm.xml.XmlArrayStringAdapter;
|
||||
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@NoArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class QueryableTypeDescriptor extends NamedClassElement {
|
||||
|
||||
@XmlElement(name = "value")
|
||||
@XmlJavaTypeAdapter(XmlArrayStringAdapter.class)
|
||||
private String[] types;
|
||||
|
||||
public String[] getTypes() {
|
||||
return types == null ? new String[0] : types;
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,9 @@ public class ScmModule {
|
||||
@XmlElement(name = "web-element")
|
||||
private Set<WebElementDescriptor> webElements;
|
||||
|
||||
@XmlElement(name = "queryable-type")
|
||||
private Set<QueryableTypeDescriptor> queryableTypes;
|
||||
|
||||
public Iterable<ClassElement> getEvents() {
|
||||
return nonNull(events);
|
||||
}
|
||||
@@ -107,12 +110,18 @@ public class ScmModule {
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
|
||||
*/
|
||||
public Iterable<ConfigElement> getConfigElements() {
|
||||
return nonNull(configElements);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public Iterable<QueryableTypeDescriptor> getQueryableTypes() {
|
||||
return nonNull(queryableTypes);
|
||||
}
|
||||
|
||||
private <T> Iterable<T> nonNull(Iterable<T> iterable) {
|
||||
if (iterable == null) {
|
||||
iterable = ImmutableSet.of();
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
|
||||
import lombok.Getter;
|
||||
import sonia.scm.repository.api.HookContext;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Repository hook event represents an change event of a repository.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
@Getter
|
||||
public class RepositoryHookEvent
|
||||
{
|
||||
|
||||
@@ -36,6 +40,13 @@ public class RepositoryHookEvent
|
||||
/** hook type */
|
||||
private final RepositoryHookType type;
|
||||
|
||||
/**
|
||||
* creation date of the event
|
||||
*
|
||||
* @since 3.8.0
|
||||
*/
|
||||
private final Instant creationDate = Instant.now();
|
||||
|
||||
public RepositoryHookEvent(HookContext context, Repository repository,
|
||||
RepositoryHookType type)
|
||||
{
|
||||
@@ -44,24 +55,6 @@ public class RepositoryHookEvent
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
public HookContext getContext()
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
public Repository getRepository()
|
||||
{
|
||||
return repository;
|
||||
}
|
||||
|
||||
|
||||
public RepositoryHookType getType()
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RepositoryHookEvent{" +
|
||||
|
||||
21
scm-core/src/main/java/sonia/scm/store/Condition.java
Normal file
21
scm-core/src/main/java/sonia/scm/store/Condition.java
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
@SuppressWarnings("unused") // We need the type 'T' here to keep type safety
|
||||
public interface Condition<T> {
|
||||
}
|
||||
38
scm-core/src/main/java/sonia/scm/store/Conditions.java
Normal file
38
scm-core/src/main/java/sonia/scm/store/Conditions.java
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
public final class Conditions {
|
||||
private Conditions() {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Condition<T> and(Condition<T>... conditions) {
|
||||
return new LogicalCondition<>(LogicalOperator.AND, conditions);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Condition<T> or(Condition<T>... conditions) {
|
||||
return new LogicalCondition<>(LogicalOperator.OR, conditions);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public static <T> Condition<T> not(Condition<T>... conditions) {
|
||||
return new LogicalCondition<>(LogicalOperator.NOT, new Condition[]{and(conditions)});
|
||||
}
|
||||
}
|
||||
50
scm-core/src/main/java/sonia/scm/store/LeafCondition.java
Normal file
50
scm-core/src/main/java/sonia/scm/store/LeafCondition.java
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* A <b>LeafCondition</b> is a condition builder on a {@link QueryableStore.QueryField} as part of a store statement.
|
||||
*
|
||||
* @param <T> type of the object held by the {@link QueryableStore.QueryField}
|
||||
* @param <C> value type (only required for binary operators)
|
||||
*/
|
||||
@Value
|
||||
@Getter
|
||||
public class LeafCondition<T, C> implements Condition<T> {
|
||||
|
||||
/**
|
||||
* Argument for the operator to check against.<br/>
|
||||
* Example: <em><strong>fruit</strong> EQ apple</em>
|
||||
*/
|
||||
QueryableStore.QueryField<T, ?> field;
|
||||
|
||||
/**
|
||||
* A binary (e.g. EQ, CONTAINS) or unary (e.g. NULL) operator. Binary operators require a non-null value field.<br/>
|
||||
* Example: <em>fruit <strong>EQ</strong> apple</em>, <em>fruit <em>NULL</em></em>
|
||||
*/
|
||||
Operator operator;
|
||||
|
||||
|
||||
/**
|
||||
* Value for binary operators.<br/>
|
||||
* Example: <em>fruit EQ <strong>apple</strong></em>
|
||||
*/
|
||||
C value;
|
||||
}
|
||||
32
scm-core/src/main/java/sonia/scm/store/LogicalCondition.java
Normal file
32
scm-core/src/main/java/sonia/scm/store/LogicalCondition.java
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
@Getter
|
||||
public class LogicalCondition<T> implements Condition<T> {
|
||||
LogicalOperator operator;
|
||||
Condition<T>[] conditions;
|
||||
|
||||
LogicalCondition(LogicalOperator operator, Condition<T>[] conditions) {
|
||||
this.operator = operator;
|
||||
this.conditions = conditions;
|
||||
}
|
||||
}
|
||||
21
scm-core/src/main/java/sonia/scm/store/LogicalOperator.java
Normal file
21
scm-core/src/main/java/sonia/scm/store/LogicalOperator.java
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
public enum LogicalOperator {
|
||||
AND, OR, NOT;
|
||||
}
|
||||
32
scm-core/src/main/java/sonia/scm/store/Operator.java
Normal file
32
scm-core/src/main/java/sonia/scm/store/Operator.java
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
public enum Operator {
|
||||
|
||||
EQ,
|
||||
LESS,
|
||||
GREATER,
|
||||
LESS_OR_EQUAL,
|
||||
GREATER_OR_EQUAL,
|
||||
CONTAINS,
|
||||
IN,
|
||||
NULL,
|
||||
KEY,
|
||||
VALUE
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import com.google.common.collect.Streams;
|
||||
import jakarta.xml.bind.annotation.XmlAccessType;
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* This store should be used only in update steps or other maintenance tasks like deleting all entries for a deleted
|
||||
* parent entity.
|
||||
*
|
||||
* @param <T> The entity type of the store.
|
||||
*/
|
||||
public interface QueryableMaintenanceStore<T> {
|
||||
|
||||
Collection<Row<T>> readAll() throws SerializationException;
|
||||
|
||||
<U> Collection<Row<U>> readAllAs(Class<U> type) throws SerializationException;
|
||||
|
||||
Collection<RawRow> readRaw();
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
default void writeAll(Iterable<Row> rows) throws SerializationException {
|
||||
writeAll(Streams.stream(rows));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
void writeAll(Stream<Row> rows) throws SerializationException;
|
||||
|
||||
default void writeRaw(Iterable<RawRow> rows) {
|
||||
writeRaw(Streams.stream(rows));
|
||||
}
|
||||
|
||||
void writeRaw(Stream<RawRow> rows);
|
||||
|
||||
@Data
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class Row<U> {
|
||||
private String[] parentIds;
|
||||
private String id;
|
||||
private U value;
|
||||
}
|
||||
|
||||
@Data
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class RawRow {
|
||||
private String[] parentIds;
|
||||
private String id;
|
||||
private String value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all entries from the store. If the store has been created limited to a concrete parent
|
||||
* or a subset of parents, only the entries for this parent(s) will be deleted.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over all entries in the store. If the store has been created limited to a concrete parent
|
||||
* or a subset of parents, only the entries for this parent(s) will be returned.
|
||||
* The iterated values offer additional methods to update or delete entries.
|
||||
* <br>
|
||||
* The iterator must be closed after usage. Otherwise, updates may not be persisted.
|
||||
*/
|
||||
MaintenanceIterator<T> iterateAll();
|
||||
|
||||
/**
|
||||
* Iterator for existing entries in the store.
|
||||
*/
|
||||
interface MaintenanceIterator<T> extends Iterator<MaintenanceStoreEntry<T>>, AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintenance helper for a concrete entry in the store.
|
||||
*/
|
||||
interface MaintenanceStoreEntry<T> {
|
||||
|
||||
/**
|
||||
* The id of the entry.
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Returns the id of the parent for the given class.
|
||||
*/
|
||||
Optional<String> getParentId(Class<?> clazz);
|
||||
|
||||
/**
|
||||
* Returns the entity as the specified type of the store.
|
||||
*
|
||||
* @throws SerializationException if the entry cannot be deserialized to the type of the store.
|
||||
*/
|
||||
T get();
|
||||
|
||||
/**
|
||||
* Returns the entry as the given type, not as the type that has been specified for the store.
|
||||
* This can be used whenever the type of the store has been changed in a way that no longer is compatible with the
|
||||
* stored data. In this case, the entry can be deserialized to a different type that is only used during the
|
||||
* migration.
|
||||
*
|
||||
* @param <U> The type of the entry.
|
||||
* @throws SerializationException if the entry cannot be deserialized to the given type.
|
||||
*/
|
||||
<U> U getAs(Class<U> type);
|
||||
|
||||
/**
|
||||
* Update the store entry with the given object.
|
||||
*
|
||||
* @throws SerializationException if the object cannot be serialized.
|
||||
*/
|
||||
void update(Object object);
|
||||
}
|
||||
|
||||
class SerializationException extends RuntimeException {
|
||||
public SerializationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
/**
|
||||
* This interface is used to store objects annotated with {@link QueryableType}.
|
||||
* It combines the functionality of a {@link DataStore} and a {@link QueryableStore}.
|
||||
* In contrast to the {@link QueryableStore}, instances are always scoped to a specific parent (if the type this store
|
||||
* is created for as parent types specified in its annotation).
|
||||
* It will be created by the {@link QueryableStoreFactory}.
|
||||
* <br/>
|
||||
* It is not meant to be instantiated by users of the API. Instead, use the query factory created by the annotation
|
||||
* processor for the annotated type.
|
||||
*
|
||||
* @param <T> The type of the objects to query.
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public interface QueryableMutableStore<T> extends DataStore<T>, QueryableStore<T>, AutoCloseable {
|
||||
void transactional(BooleanSupplier callback);
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
668
scm-core/src/main/java/sonia/scm/store/QueryableStore.java
Normal file
668
scm-core/src/main/java/sonia/scm/store/QueryableStore.java
Normal file
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This interface is used to query objects annotated with {@link QueryableType}. It will be created by the
|
||||
* {@link QueryableStoreFactory}.
|
||||
* <br/>
|
||||
* It is not meant to be instantiated by users of the API. Instead, use the query factory created by the annotation
|
||||
* processor for the annotated type.
|
||||
*
|
||||
* @param <T> The type of the objects to query.
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public interface QueryableStore<T> extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Creates a query for the objects of the type {@code T} with the given conditions. Conditions should be created by
|
||||
* either using the static methods of the {@link Conditions} class or by using the query fields of the type {@code T}
|
||||
* that will be created by the annotation processor in a separate class. If your annotated type is named
|
||||
* {@code MyType}, the query fields class will be named {@code MyTypeQueryFields}.
|
||||
* <br/>
|
||||
* If no conditions are given, all objects of the type {@code T} will be returned (limited by the ids of the
|
||||
* parent objects that had been specified when this instance of the store had been created by the factory). If more
|
||||
* than one condition is given, the conditions will be combined with a logical AND.
|
||||
*
|
||||
* @param conditions The conditions to filter the objects.
|
||||
* @return The query object to retrieve the result.
|
||||
*/
|
||||
Query<T, T> query(Condition<T>... conditions);
|
||||
|
||||
/**
|
||||
* Used to specify the order of the result of a query.
|
||||
*/
|
||||
enum Order {
|
||||
/**
|
||||
* Ascending order.
|
||||
*/
|
||||
ASC,
|
||||
/**
|
||||
* Descending order.
|
||||
*/
|
||||
DESC
|
||||
}
|
||||
|
||||
/**
|
||||
* The terminal interface for a query build by {@link #query(Condition[])}. It provides methods to retrieve the
|
||||
* result of the query in different forms.
|
||||
*
|
||||
* @param <T> The type of the objects to query.
|
||||
* @param <T_RESULT> The type of the result objects (if a projection had been made, for example using
|
||||
* {@link #withIds()}).
|
||||
*/
|
||||
interface Query<T, T_RESULT> {
|
||||
|
||||
/**
|
||||
* Returns the first found object, if the query returns at least one result.
|
||||
* If the query returns no result, an empty optional will be returned.
|
||||
*/
|
||||
Optional<T_RESULT> findFirst();
|
||||
|
||||
/**
|
||||
* Returns the found object, if the query returns one exactly one result. When the query returns more than one
|
||||
* result, a {@link TooManyResultsException} will be thrown. If the query returns no result, an empty optional will be returned.
|
||||
*/
|
||||
Optional<T_RESULT> findOne() throws TooManyResultsException;
|
||||
|
||||
/**
|
||||
* Returns all objects that match the query. If the query returns no result, an empty list will be returned.
|
||||
*/
|
||||
List<T_RESULT> findAll();
|
||||
|
||||
/**
|
||||
* Returns a subset of all objects that match the query. If the query returns no result or the {@code offset} and
|
||||
* {@code limit} are set in a way, that the result is exceeded, an empty list will be returned.
|
||||
*
|
||||
* @param offset The offset to start the result list.
|
||||
* @param limit The maximum number of results to return.
|
||||
*/
|
||||
List<T_RESULT> findAll(long offset, long limit);
|
||||
|
||||
/**
|
||||
* Returns the found objects in combination with the parent ids they belong to. This is useful if you are using a
|
||||
* queryable store that is not scoped to specific parent objects, and you therefore want to know to which parent
|
||||
* objects each of the found objects belong to.
|
||||
*
|
||||
* @return The query object to continue building the query.
|
||||
*/
|
||||
Query<T, Result<T_RESULT>> withIds();
|
||||
|
||||
/**
|
||||
* Orders the result by the given field in the given order. If the order is not set, the order of the result is not
|
||||
* specified. Orders can be chained, so you can call this method multiple times to order by multiple fields.
|
||||
*
|
||||
* @param field The field to order by.
|
||||
* @param order The order to use (either ascending or descending).
|
||||
* @return The query object to continue building the query.
|
||||
*/
|
||||
Query<T, T_RESULT> orderBy(QueryField<T, ?> field, Order order);
|
||||
|
||||
/**
|
||||
* Returns the count of all objects that match the query.
|
||||
*/
|
||||
long count();
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a query that was built by {@link QueryableStore.Query#withIds()}. It contains the parent ids of the
|
||||
* found objects in addition to the objects and their ids themselves.
|
||||
*
|
||||
* @param <T> The type of the queried objects.
|
||||
*/
|
||||
interface Result<T> {
|
||||
/**
|
||||
* Returns the parent ids of the found objects. The parent ids are ordered in the same way as their types are
|
||||
* specified in the @{@link QueryableType} annotation for the queried type.
|
||||
*/
|
||||
Optional<String> getParentId(Class<?> clazz);
|
||||
|
||||
/**
|
||||
* Returns the id of the found object.
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Returns the found object itself.
|
||||
*/
|
||||
T getEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of this class will be created by the annotation processor for each class annotated with
|
||||
* {@link QueryableType}. It provides query fields for the annotated class to build queries with.
|
||||
* <br/>
|
||||
* This is not meant to be extended or instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this field is used for.
|
||||
* @param <F> The type of the field.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
class QueryField<T, F> {
|
||||
final String name;
|
||||
|
||||
public QueryField(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isIdField() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is null.
|
||||
*
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> isNull() {
|
||||
return new LeafCondition<>(this, Operator.NULL, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each {@link String} field of a class annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class StringQueryField<T> extends QueryField<T, String> {
|
||||
|
||||
public StringQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public LeafCondition<T, String> eq(String value) {
|
||||
return new LeafCondition<>(this, Operator.EQ, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field contains the given value as a substring.
|
||||
*
|
||||
* @param value The value to check for.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> contains(String value) {
|
||||
return new LeafCondition<>(this, Operator.CONTAINS, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to any of the given values.
|
||||
*
|
||||
* @param values The values to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> in(String... values) {
|
||||
return new LeafCondition<>(this, Operator.IN, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to any of the given values.
|
||||
*
|
||||
* @param values The values to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> in(Collection<String> values) {
|
||||
return in(values.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for a class annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class IdQueryField<T> extends StringQueryField<T> {
|
||||
public IdQueryField(Class<?> clazz) {
|
||||
super(clazz.getName());
|
||||
}
|
||||
|
||||
public IdQueryField() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdField() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each number field (either {@link Integer}, {@link Long}, {@code int}, or {@code long}) of a class
|
||||
* annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
* @param <N> The type of the number field.
|
||||
*/
|
||||
class NumberQueryField<T, N extends Number> extends QueryField<T, N> {
|
||||
|
||||
public NumberQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> eq(N value) {
|
||||
return new LeafCondition<>(this, Operator.EQ, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is greater than the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> greater(N value) {
|
||||
return new LeafCondition<>(this, Operator.GREATER, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is less than the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> less(N value) {
|
||||
return new LeafCondition<>(this, Operator.LESS, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is greater than or equal to the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> greaterOrEquals(N value) {
|
||||
return new LeafCondition<>(this, Operator.GREATER_OR_EQUAL, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is less than or equal to the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> lessOrEquals(N value) {
|
||||
return new LeafCondition<>(this, Operator.LESS_OR_EQUAL, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the fields is inclusively between the from and to values.
|
||||
*
|
||||
* @param from The lower limit to compare the value with.
|
||||
* @param to The upper limit to compare the value with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> between(N from, N to) {
|
||||
return Conditions.and(lessOrEquals(to), greaterOrEquals(from));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to any of the given values.
|
||||
*
|
||||
* @param values The values to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> in(N... values) {
|
||||
return new LeafCondition<>(this, Operator.IN, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to any of the given values.
|
||||
*
|
||||
* @param values The values to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> in(Collection<N> values) {
|
||||
return new LeafCondition<>(this, Operator.IN, values.toArray(new Object[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each date field of a class annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class InstantQueryField<T> extends QueryField<T, Instant> {
|
||||
public InstantQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to the given value. The given instant will be truncated to
|
||||
* milliseconds.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> eq(Instant value) {
|
||||
return new LeafCondition<>(this, Operator.EQ, value.truncatedTo(ChronoUnit.MILLIS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is after the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> after(Instant value) {
|
||||
return new LeafCondition<>(this, Operator.GREATER, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is before the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> before(Instant value) {
|
||||
return new LeafCondition<>(this, Operator.LESS, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is between the given values.
|
||||
*
|
||||
* @param from The lower bound of the range to compare the field with.
|
||||
* @param to The upper bound of the range to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> between(Instant from, Instant to) {
|
||||
return Conditions.and(after(from), before(to));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> eq(Date value) {
|
||||
return eq(value.toInstant());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is after the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> after(Date value) {
|
||||
return after(value.toInstant());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is before the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> before(Date value) {
|
||||
return before(value.toInstant());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is between the given values.
|
||||
*
|
||||
* @param from The lower bound of the range to compare the field with.
|
||||
* @param to The upper bound of the range to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> between(Date from, Date to) {
|
||||
return between(from.toInstant(), to.toInstant());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each boolean field of a class annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class BooleanQueryField<T> extends QueryField<T, Boolean> {
|
||||
|
||||
public BooleanQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to the given value.
|
||||
*
|
||||
* @param b The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> eq(Boolean b) {
|
||||
return new LeafCondition<>(this, Operator.EQ, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is true.
|
||||
*
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> isTrue() {
|
||||
return eq(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is false.
|
||||
*
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> isFalse() {
|
||||
return eq(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each enum field of a class annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
* @param <E> The type of the enum field.
|
||||
*/
|
||||
class EnumQueryField<T, E extends Enum<E>> extends QueryField<T, Enum<E>> {
|
||||
public EnumQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to the given value.
|
||||
*
|
||||
* @param value The value to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public LeafCondition<T, String> eq(E value) {
|
||||
return new LeafCondition<>(this, Operator.EQ, value.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to any of the given values.
|
||||
*
|
||||
* @param values The values to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> in(E... values) {
|
||||
return new LeafCondition<>(this, Operator.IN, Arrays.stream(values).map(Enum::name).toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field is equal to any of the given values.
|
||||
*
|
||||
* @param values The values to compare the field with.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> in(Collection<E> values) {
|
||||
return new LeafCondition<>(this, Operator.IN, values.stream().map(Enum::name).toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each collection field of a class annotated with {@link QueryableType}. Note that this can only be
|
||||
* used for collections of base types like {@link String}, number types, enums or booleans.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class CollectionQueryField<T> extends QueryField<T, Object> {
|
||||
public CollectionQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field contains the given value.
|
||||
*
|
||||
* @param value The value to check for.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> contains(Object value) {
|
||||
return new LeafCondition<>(this, Operator.EQ, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries, based on the size of a collection.
|
||||
* Instances of this class will be created by the annotation processor for each collection
|
||||
* field of a class annotated with {@link QueryableType}. Note that this can only be used
|
||||
* for collections of base types like {@link String}, number types, enums or booleans.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class CollectionSizeQueryField<T> extends NumberQueryField<T, Long> {
|
||||
public CollectionSizeQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the collection field is empty.
|
||||
*
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> isEmpty() {
|
||||
return eq(0L);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
|
||||
* processor for each map field of a class annotated with {@link QueryableType}. Note that this can only be used for
|
||||
* maps with base types like {@link String}, number types, enums or booleans as keys.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class MapQueryField<T> extends QueryField<T, Object> {
|
||||
public MapQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field contains the given key.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> containsKey(Object key) {
|
||||
return new LeafCondition<>(this, Operator.KEY, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the field contains the given value.
|
||||
*
|
||||
* @param value The value to check for.
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> containsValue(Object value) {
|
||||
return new LeafCondition<>(this, Operator.VALUE, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create conditions for queries, based on the size of a map.
|
||||
* Instances of this class will be created by the annotation processor for each map
|
||||
* field of a class annotated with {@link QueryableType}. Note that this can only be used
|
||||
* for collections of base types like {@link String}, number types, enums or booleans.
|
||||
* <br/>
|
||||
* This is not meant to be instantiated by users of the API!
|
||||
*
|
||||
* @param <T> The type of the objects this condition is used for.
|
||||
*/
|
||||
class MapSizeQueryField<T> extends NumberQueryField<T, Long> {
|
||||
public MapSizeQueryField(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition that checks if the map field is empty.
|
||||
*
|
||||
* @return The condition to use in a query.
|
||||
*/
|
||||
public Condition<T> isEmpty() {
|
||||
return eq(0L);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception occurring, if the client queried for one result with {@link Query#findOne()}, but the query returned multiple results.
|
||||
*/
|
||||
class TooManyResultsException extends RuntimeException {
|
||||
public TooManyResultsException() {
|
||||
super("Found more than one result");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
/**
|
||||
* Factory to create {@link QueryableStore} and {@link QueryableMutableStore} instances.
|
||||
* In comparison to the {@link DataStoreFactory}, this factory is used to create stores which can execute
|
||||
* queries on the stored data. Queryable stores can be used for types which are annotated with {@link QueryableType}.
|
||||
* <br/>
|
||||
* Normally, there should be no need to use this factory directly. Instead, for each type annotated with
|
||||
* {@link QueryableType} a dedicated store factory is generated which can be injected into other components.
|
||||
* For instance, if your data class is named {@code MyData} and annotated with {@link QueryableType}, a factory
|
||||
* you should find a {@code MyDataStoreFactory} for your needs which is backed by this class.
|
||||
* <br/>
|
||||
* Implementations probably are backed by a database or a similar storage system instead of the familiar
|
||||
* file based storage using XML.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public interface QueryableStoreFactory {
|
||||
|
||||
/**
|
||||
* Creates a read-only store for the given class and optional parent ids. If parent ids are omitted, queries
|
||||
* will not be restricted to a specific parent (for example a repository) but will run on all data of the given type.
|
||||
*
|
||||
* @param clazz The class of the data type (must be annotated with {@link QueryableType}).
|
||||
* @param parentIds Optional parent ids to restrict the query to a specific parent.
|
||||
* @param <T> The type of the data.
|
||||
* @return A read-only store for the given class and optional parent ids.
|
||||
*/
|
||||
<T> QueryableStore<T> getReadOnly(Class<T> clazz, String... parentIds);
|
||||
|
||||
/**
|
||||
* Creates a mutable store for the given class and parent ids. In contrast to the read-only store, for a mutable store
|
||||
* the parent ids are mandatory. For each parent class given in the {@link QueryableType} annotation of the type, a
|
||||
* concrete id has to be specified. This is because mutable stores are used to store data, which is done for the
|
||||
* concrete parents only. So if data should be stored for different parents, separate mutable stores have to be
|
||||
* created.
|
||||
* <br/>
|
||||
* The mutable store provides methods to store, update and delete data but also all query methods of the read-only
|
||||
* store.
|
||||
*
|
||||
* @param clazz The class of the data type (must be annotated with {@link QueryableType}).
|
||||
* @param parentIds Ids for all parent classes named in the {@link QueryableType} annotation.
|
||||
* @param <T> The type of the data.
|
||||
* @return A mutable store for the given class scoped to the given parents.
|
||||
*/
|
||||
<T> QueryableMutableStore<T> getMutable(Class<T> clazz, String... parentIds);
|
||||
|
||||
<T> QueryableMaintenanceStore<T> getForMaintenance(Class<T> clazz, String... parentIds);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
@ExtensionPoint
|
||||
public interface StoreDeletionNotifier {
|
||||
void registerHandler(DeletionHandler handler);
|
||||
|
||||
interface DeletionHandler {
|
||||
default void notifyDeleted(Class<?> clazz, String id) {
|
||||
notifyDeleted(new ClassWithId(clazz, id));
|
||||
}
|
||||
void notifyDeleted(ClassWithId... classWithIds);
|
||||
}
|
||||
|
||||
record ClassWithId(Class<?> clazz, String id) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface StoreMetaDataProvider {
|
||||
Collection<Class<?>> getTypesWithParent(Class<?>... classes);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import sonia.scm.store.QueryableMaintenanceStore;
|
||||
import sonia.scm.store.StoreParameters;
|
||||
import sonia.scm.store.StoreType;
|
||||
|
||||
@@ -25,6 +26,8 @@ public interface StoreUpdateStepUtilFactory {
|
||||
return new UtilForTypeBuilder(this, type);
|
||||
}
|
||||
|
||||
<T> QueryableMaintenanceStore<T> forQueryableType(Class<T> clazz, String... parents);
|
||||
|
||||
final class UtilForTypeBuilder {
|
||||
private final StoreUpdateStepUtilFactory factory;
|
||||
private final StoreType type;
|
||||
|
||||
68
scm-it/src/test/java/sonia/scm/it/ExportITCase.java
Normal file
68
scm-it/src/test/java/sonia/scm/it/ExportITCase.java
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.it;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.ScmRequests;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
|
||||
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
|
||||
|
||||
public class ExportITCase {
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
TestData.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldExportAndImportRepository() {
|
||||
String namespace = ADMIN_USERNAME;
|
||||
TestData.createDefault();
|
||||
String repo = TestData.getDefaultRepoName("git");
|
||||
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.requestRepository(namespace, repo)
|
||||
.writeTestData("value");
|
||||
|
||||
Path exportFile = ScmRequests.start()
|
||||
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.requestRepository(namespace, repo)
|
||||
.requestFullExport()
|
||||
.exportFile();
|
||||
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.requestRepositoryType("git")
|
||||
.requestImport("fullImport", exportFile, "imported");
|
||||
|
||||
List<String> importedTestData = ScmRequests.start()
|
||||
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.requestRepository(namespace, "imported")
|
||||
.requestTestData()
|
||||
.getTestData();
|
||||
|
||||
assertThat(importedTestData).containsExactly("value");
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,23 @@ package sonia.scm.it.utils;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import org.junit.Assert;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.api.v2.resources.RepositoryDto;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
|
||||
|
||||
@@ -41,6 +49,7 @@ import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
|
||||
* that return the *Response class containing specific operations related to the specific response
|
||||
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class ScmRequests {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScmRequests.class);
|
||||
@@ -65,13 +74,13 @@ public class ScmRequests {
|
||||
public UserResponse<UserResponse> requestUser(String username, String password, String pathParam) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/"+pathParam).toString()), null);
|
||||
return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/" + pathParam).toString()), null);
|
||||
}
|
||||
|
||||
public ChangePasswordResponse<ChangePasswordResponse> requestUserChangePassword(String username, String password, String userPathParam, String newPassword) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password,newPassword)), null);
|
||||
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/" + userPathParam + "/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password, newPassword)), null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -80,6 +89,11 @@ public class ScmRequests {
|
||||
return new ModelResponse(response, null);
|
||||
}
|
||||
|
||||
public RequestSpecification withBasicAuth() {
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the extracted url from the given link
|
||||
*
|
||||
@@ -117,13 +131,12 @@ public class ScmRequests {
|
||||
*/
|
||||
private Response applyGETRequestWithQueryParams(String url, String params) {
|
||||
LOG.info("GET {}", url);
|
||||
if (username == null || password == null){
|
||||
if (username == null || password == null) {
|
||||
return RestAssured.given()
|
||||
.when()
|
||||
.get(url + params);
|
||||
}
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
return withBasicAuth()
|
||||
.when()
|
||||
.get(url + params);
|
||||
}
|
||||
@@ -138,13 +151,11 @@ public class ScmRequests {
|
||||
return applyGETRequestWithQueryParams(url, "");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a PUT Request to the extracted url from the given link
|
||||
*
|
||||
* @param response the response containing the link
|
||||
* @param linkPropertyName the property name of link
|
||||
* @param body
|
||||
* @return the response of the PUT request using the given link
|
||||
*/
|
||||
private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) {
|
||||
@@ -158,15 +169,12 @@ public class ScmRequests {
|
||||
/**
|
||||
* Apply a PUT Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the PUT request
|
||||
* @param mediaType
|
||||
* @param body
|
||||
* @param url the url of the PUT request
|
||||
* @return the response of the PUT request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyPUTRequest(String url, String mediaType, String body) {
|
||||
LOG.info("PUT {}", url);
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
return withBasicAuth()
|
||||
.when()
|
||||
.contentType(mediaType)
|
||||
.accept(mediaType)
|
||||
@@ -174,6 +182,37 @@ public class ScmRequests {
|
||||
.put(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a PUT Request to the extracted url from the given link
|
||||
*
|
||||
* @param response the response containing the link
|
||||
* @param linkPropertyName the property name of link
|
||||
* @return the response of the PUT request using the given link
|
||||
*/
|
||||
private Response applyPOSTRequestFromLink(Response response, String linkPropertyName, String content, String body) {
|
||||
return applyPOSTRequest(response
|
||||
.then()
|
||||
.extract()
|
||||
.path(linkPropertyName), content, body);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a POST Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the PUT request
|
||||
* @return the response of the PUT request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyPOSTRequest(String url, String mediaType, String body) {
|
||||
LOG.info("POST {}", url);
|
||||
return withBasicAuth()
|
||||
.when()
|
||||
.contentType(mediaType)
|
||||
.accept(mediaType)
|
||||
.body(body)
|
||||
.post(url);
|
||||
}
|
||||
|
||||
private void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
@@ -186,6 +225,7 @@ public class ScmRequests {
|
||||
public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href";
|
||||
public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href";
|
||||
public static final String LINK_REPOSITORIES = "_links.repositories.href";
|
||||
public static final String LINK_REPOSITORY_TYPES = "_links.repositoryTypes.href";
|
||||
private static final String LINK_ME = "_links.me.href";
|
||||
private static final String LINK_USERS = "_links.users.href";
|
||||
|
||||
@@ -201,6 +241,10 @@ public class ScmRequests {
|
||||
return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_GROUPS, "?q=" + q), this);
|
||||
}
|
||||
|
||||
public RepositoryTypeResponse<IndexResponse> requestRepositoryType(String type) {
|
||||
return new RepositoryTypeResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORY_TYPES, type), this);
|
||||
}
|
||||
|
||||
public RepositoryResponse<IndexResponse> requestRepository(String namespace, String name) {
|
||||
return new RepositoryResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORIES, namespace + "/" + name), this);
|
||||
}
|
||||
@@ -221,13 +265,12 @@ public class ScmRequests {
|
||||
return response
|
||||
.then()
|
||||
.extract()
|
||||
.path("_links." + linkName + ".href");
|
||||
.path("_links." + linkName + ".href");
|
||||
}
|
||||
}
|
||||
|
||||
public class RepositoryResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> {
|
||||
|
||||
|
||||
public static final String LINKS_SOURCES = "_links.sources.href";
|
||||
public static final String LINKS_CHANGESETS = "_links.changesets.href";
|
||||
|
||||
@@ -243,6 +286,45 @@ public class ScmRequests {
|
||||
return new ChangesetsResponse<>(applyGETRequestFromLink(response, LINKS_CHANGESETS), this);
|
||||
}
|
||||
|
||||
public FullExportResponse<RepositoryResponse> requestFullExport() {
|
||||
return new FullExportResponse<>(applyGETRequestFromLinkWithParams(response, "_links.fullExport.href", "?compressed=true"), this);
|
||||
}
|
||||
|
||||
public void writeTestData(String value) {
|
||||
applyPOSTRequestFromLink(response, "_links.test-data.href", APPLICATION_JSON, "{\"value\":\"" + value + "\"}");
|
||||
}
|
||||
|
||||
public TestDataResponse<RepositoryResponse> requestTestData() {
|
||||
return new TestDataResponse<>(applyGETRequestFromLink(response, "_links.test-data.href"), this);
|
||||
}
|
||||
}
|
||||
|
||||
public class RepositoryTypeResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> {
|
||||
|
||||
public RepositoryTypeResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public void requestImport(String type, Path file, String repositoryName) {
|
||||
String url = response
|
||||
.then()
|
||||
.extract()
|
||||
.path("_links.import.find{it.name=='" + type + "'}.href");
|
||||
Assert.assertNotNull("no url found for link " + "_links.import.find{it.name=='" + type + "'}.href", url);
|
||||
|
||||
LOG.info("POST for import to {}", url);
|
||||
RepositoryDto repository = new RepositoryDto();
|
||||
repository.setType("git");
|
||||
repository.setName(repositoryName);
|
||||
withBasicAuth()
|
||||
.multiPart("bundle", file.toFile())
|
||||
.multiPart("repository", repository)
|
||||
.when()
|
||||
.post(url + "?compressed=true")
|
||||
.then()
|
||||
.statusCode(201);
|
||||
System.out.println("done");
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangesetsResponse<PREV extends ModelResponse> extends ModelResponse<ChangesetsResponse<PREV>, PREV> {
|
||||
@@ -294,6 +376,35 @@ public class ScmRequests {
|
||||
}
|
||||
}
|
||||
|
||||
public class FullExportResponse<PREV extends ModelResponse> extends ModelResponse<FullExportResponse<PREV>, PREV> {
|
||||
|
||||
public FullExportResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public Path exportFile() {
|
||||
InputStream exportStream = response.asInputStream();
|
||||
try {
|
||||
Path tempFile = Files.createTempFile("scm-export", ".tgz");
|
||||
Files.copy(exportStream, tempFile, REPLACE_EXISTING);
|
||||
return tempFile;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TestDataResponse<PREV extends ModelResponse> extends ModelResponse<FullExportResponse<PREV>, PREV> {
|
||||
|
||||
public TestDataResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public List<String> getTestData() {
|
||||
return response.then().contentType(APPLICATION_JSON).extract().path("value");
|
||||
}
|
||||
}
|
||||
|
||||
public class ModificationsResponse<PREV extends ModelResponse> extends ModelResponse<ModificationsResponse<PREV>, PREV> {
|
||||
|
||||
public ModificationsResponse(Response response, PREV previousResponse) {
|
||||
@@ -382,7 +493,7 @@ public class ScmRequests {
|
||||
this.previousResponse = previousResponse;
|
||||
}
|
||||
|
||||
public Response getResponse(){
|
||||
public Response getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ plugins {
|
||||
dependencies {
|
||||
implementation libraries.commonsIo
|
||||
implementation libraries.commonsLang3
|
||||
implementation libraries.sqlite
|
||||
|
||||
api platform(project(':'))
|
||||
|
||||
@@ -31,5 +32,7 @@ dependencies {
|
||||
|
||||
// lombok
|
||||
compileOnly libraries.lombok
|
||||
testCompileOnly libraries.lombok
|
||||
annotationProcessor libraries.lombok
|
||||
testAnnotationProcessor libraries.lombok
|
||||
}
|
||||
@@ -14,11 +14,12 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm;
|
||||
|
||||
import com.google.common.util.concurrent.Striped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.store.StoreException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -24,13 +24,13 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.CopyOnWrite;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.CopyOnWrite;
|
||||
import sonia.scm.store.file.StoreConstants;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static sonia.scm.store.CopyOnWrite.compute;
|
||||
import static sonia.scm.CopyOnWrite.compute;
|
||||
|
||||
public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
||||
|
||||
@@ -27,7 +27,7 @@ import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.BasicRepositoryLocationResolver;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.store.file.StoreConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -89,7 +89,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
||||
public <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
||||
if (type.isAssignableFrom(Path.class)) {
|
||||
return (RepositoryLocationResolverInstance<T>) new RepositoryLocationResolverInstance<Path>() {
|
||||
@Override
|
||||
@@ -20,7 +20,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.store.CopyOnWrite;
|
||||
import sonia.scm.CopyOnWrite;
|
||||
import sonia.scm.xml.XmlStreams;
|
||||
import sonia.scm.xml.XmlStreams.AutoCloseableXMLReader;
|
||||
import sonia.scm.xml.XmlStreams.AutoCloseableXMLWriter;
|
||||
@@ -35,7 +35,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
|
||||
import static sonia.scm.store.CopyOnWrite.execute;
|
||||
import static sonia.scm.CopyOnWrite.execute;
|
||||
|
||||
class PathDatabase {
|
||||
|
||||
@@ -24,8 +24,12 @@ import java.util.function.BiConsumer;
|
||||
|
||||
public class SingleRepositoryUpdateProcessor {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
private RepositoryLocationResolver locationResolver;
|
||||
public SingleRepositoryUpdateProcessor(RepositoryLocationResolver locationResolver) {
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
public void doUpdate(BiConsumer<String, Path> forEachRepository) {
|
||||
locationResolver.forClass(Path.class).forAllLocations(forEachRepository);
|
||||
@@ -19,17 +19,20 @@ package sonia.scm.store;
|
||||
import jakarta.inject.Inject;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.store.file.FileStoreUpdateStepUtil;
|
||||
import sonia.scm.update.StoreUpdateStepUtilFactory;
|
||||
|
||||
public class FileStoreUpdateStepUtilFactory implements StoreUpdateStepUtilFactory {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final QueryableStoreFactory queryableStoreFactory;
|
||||
|
||||
@Inject
|
||||
public FileStoreUpdateStepUtilFactory(RepositoryLocationResolver locationResolver, SCMContextProvider contextProvider) {
|
||||
public FileStoreUpdateStepUtilFactory(RepositoryLocationResolver locationResolver, SCMContextProvider contextProvider, QueryableStoreFactory queryableStoreFactory) {
|
||||
this.locationResolver = locationResolver;
|
||||
this.contextProvider = contextProvider;
|
||||
this.queryableStoreFactory = queryableStoreFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,4 +40,8 @@ public class FileStoreUpdateStepUtilFactory implements StoreUpdateStepUtilFactor
|
||||
return new FileStoreUpdateStepUtil(locationResolver, contextProvider, parameters, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> QueryableMaintenanceStore<T> forQueryableType(Class<T> clazz, String... parents) {
|
||||
return queryableStoreFactory.getForMaintenance(clazz, parents);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package sonia.scm.store;
|
||||
import jakarta.inject.Inject;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.store.file.FileBasedStoreEntryImporterFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm.store.file;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -29,7 +29,7 @@ import java.io.File;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Singleton
|
||||
public class DataFileCache {
|
||||
class DataFileCache {
|
||||
|
||||
private static final String CACHE_NAME = "sonia.cache.dataFileCache";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DataFileCache.class);
|
||||
@@ -14,7 +14,7 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm.store.file;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
@@ -14,9 +14,10 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm.store.file;
|
||||
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
import sonia.scm.store.Exporter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@@ -14,7 +14,10 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm.store.file;
|
||||
|
||||
import sonia.scm.store.ExportableStore;
|
||||
import sonia.scm.store.StoreType;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
@@ -14,7 +14,12 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm.store.file;
|
||||
|
||||
import sonia.scm.store.ExportableStore;
|
||||
import sonia.scm.store.Exporter;
|
||||
import sonia.scm.store.StoreEntryMetaData;
|
||||
import sonia.scm.store.StoreType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -23,7 +28,7 @@ import java.util.function.Function;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.store.ExportCopier.putFileContentIntoStream;
|
||||
import static sonia.scm.store.file.ExportCopier.putFileContentIntoStream;
|
||||
|
||||
class ExportableConfigEntryFileStore implements ExportableStore {
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.store;
|
||||
package sonia.scm.store.file;
|
||||
|
||||
import sonia.scm.store.ExportableStore;
|
||||
import sonia.scm.store.Exporter;
|
||||
import sonia.scm.store.StoreEntryMetaData;
|
||||
import sonia.scm.store.StoreType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -23,7 +28,7 @@ import java.util.function.Function;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.store.ExportCopier.putFileContentIntoStream;
|
||||
import static sonia.scm.store.file.ExportCopier.putFileContentIntoStream;
|
||||
|
||||
class ExportableConfigFileStore implements ExportableStore {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user