I'm implementing an API that uses an update_mask of type Field Mask to specify the fields that should be updated on a resource.
The API is similar to:
rpc UpdateBook(UpdateBookRequest) returns (Book)
message UpdateBookRequest {
Book book=1;
google.protobuf.FieldMask update_mask = 2;
}
Currently the code looks something like:
override suspend fun updateBook(request: UpdateBookRequest): Book {
if (!FieldMaskUtil.isValid(Book::class.java, request.updateMask)) {
throwStatusRuntimeException {
code = Code.INVALID_ARGUMENT_VALUE
message = "Invalid field mask"
}
}
request.book.let { book ->
request.updateMask.pathsList.let { updateMask ->
var displayName: BookDisplayNameValue? = null
var description: BookDescriptionValue? = null
if (updateMask.contains(BookFields.DISPLAY_NAME_FIELD)) {
displayName = BookDisplayNameValue(book.displayName)
}
if (updateMask.contains(BookFields.DESCRIPTION_FIELD)) {
description = BookDescriptionValue(book.description.ifBlank { null })
}
bookService.update(
bookId = BookIdValue(book.name),
displayName = displayName,
description = description,
)
}
}
//...
}
Which, although it works, is pretty horrendous for maintenance as more fields are added.
Are there any best practices as to how to implement this behaviour on the server (Kotlin + Spring framework + GRPC) that can reduce the amount of boilerplate.
Related
I have a kotlin multi platform project which contains apollo graphql api
in this project i have BaseRepository Class and in this class there is a method to execute query or mutations
suspend fun <D : Query.Data> executeQuery(query: Query<D>): ApolloResponse<D> {
val response = getApolloClient().query(query).execute()
checkOperation(response)
return response
}
suspend fun <D : Mutation.Data> executeMutation(mutation: Mutation<D>): ApolloResponse<D> {
val response = getApolloClient().mutation(mutation).execute()
checkOperation(response)
return response
}
For example i want to use this method in Some repository like this
class HelpRepository : BaseRepository() {
fun test(request: AddFeedBackRequest) = flow {
val feedBackType = if (request.type == AddFeedBackType.Bug) {
FeedbackType.BUG
} else {
FeedbackType.FEEDBACK
}
val input = AddFeedbackInput(request.note, Optional.presentIfNotNull(feedBackType))
emit(true)
val mutation = AddFeedbackMutation(input)
val response = executeMutation(mutation)
emit(false)
}
}
when i add the flow scope i shouldn't be had to convert this method to a suspend function
i dont want to use suspend function because of ios application. When i use suspend function its convert "Kotlinx_coroutines_coreFlowCollector" in xcode
so i found a wrapper function like this
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun listen(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
when i use this wrapper with single variable it works exactly what i want in xcode.
but in functions i couldn't find a proper way to do this
i need a wrapper like
= commonFlow {
}
instead of this
= flow {
}
to use this method as a commonFlow wrapper
Can you help me ?
We have pretty much the same thing in one of our projects. We have a extension function that converts the regular flow to a "common" flow so it can be used in both Android and iOS.
You can created flow like always, and wrap it at the end.
fun <T> Flow<T>.wrap(): CommonFlow<T> = CommonFlow(this)
class HelpRepository : BaseRepository() {
fun test(request: AddFeedBackRequest) = flow {
val feedBackType = if (request.type == AddFeedBackType.Bug) {
FeedbackType.BUG
} else {
FeedbackType.FEEDBACK
}
val input = AddFeedbackInput(request.note, Optional.presentIfNotNull(feedBackType))
emit(true)
val mutation = AddFeedbackMutation(input)
val response = executeMutation(mutation)
emit(false)
}
}.wrap()
This is a query I wrote:
public List<PrRevamps> fetchRevampHistory(String refNo) {
List<PrRevamps> list = dsl.select(
DSL.concat(
Tables.PR_REVAMPS.PR_REVAMP_CODE,DSL.val("-"),
Tables.PR_REVAMP_CODES.PR_DESCRIPTION
).as("DESCRIPTION"),
Tables.PR_REVAMPS.PR_REVAMP_DATE,
Tables.PR_REVAMPS.PR_KEY)
.from(Tables.PR_REVAMP_CODES,Tables.PR_REVAMPS)
.where(Tables.PR_REVAMPS.PR_REVAMP_CODE.equal(Tables.PR_REVAMP_CODES.PR_CODE))
.and(Tables.PR_REVAMPS.PR_PROP_REF_NO.equal(refNo))
.fetchInto(PrRevamps.class);
return list;
}
and I need to display revampCode and description in one row in vaadin grid.
i.e "A-Marshall"
This is the grid code below:
private void populateRevampGrid() {
Object ref = serv.getAttribute("ref");
String propRef = "";
if(ref != null) {
propRef = (String)ref;
List<PrRevamps> list = getService().fetchRevampHistory(propRef);
if(!list.isEmpty()) {
revampGrid.setContainerDataSource(new BeanItemContainer<>(list));
//revampGrid.addColumn("prRevampCode").setHeaderCaption("REVAMP DESCRIPTION");
revampGrid.removeColumn("prKey");
revampGrid.removeColumn("prLeaseType");
revampGrid.removeColumn("prPropRefNo");
revampGrid.getColumn("prRevampDate").setHeaderCaption("REVAMP DATE");
revampGrid.getColumn("prRevampCode").setHeaderCaption("REVAMP TYPE");
} else {
clearfields();
Notification.show("list is empty",Type.ERROR_MESSAGE.WARNING_MESSAGE);
}
}
}
As I understand, you have the description field, that has the desired value.
If not, you can add properties, generated from an object on the fly. But for this, you will need to use Container.
The full description can be found on Vaadin documentation https://vaadin.com/docs/v7/framework/datamodel/datamodel-container.html (see GeneratedPropertyContainer)
Hope you are all healthy!
I have a situation in which I have a backend application that exposes a rest api to create data related to expense reporting. The main entity that this API allows to be created is an "Expense Report" which has other entities related to it, such as the country on which the expenses took place and the user that created it.
Thing is, my controller receives a DTO, converts it into a JPA entity and then sends it to the service class. In my service class, I have to check if a related entity field like the username for User or the country code for Country, and then go into the corresponding entity repository and get the corresponding entity.
public ExpenseReport save(ExpenseReport expenseReport) {
if (expenseReport.getId() != null) {
expenseReportRepository.findById(expenseReport.getId())
.ifPresent(currentObject -> {
expenseReport.setId(currentObject.getId());
expenseReport.setVersion(currentObject.getVersion());
});
}
if (expenseReport.getUser() != null && expenseReport.getUser().getUsername() != null) {
String username = expenseReport.getUser().getUsername();
userRepository.findByUsername(username)
.ifPresentOrElse(user -> {
expenseReport.setUser(user);
},
() -> {
throw new InvalidDataException(User.class, "username", username);
});
}
if (expenseReport.getCountry() != null && expenseReport.getCountry().getCode() != null) {
String countryCode = expenseReport.getCountry().getCode();
countryRepository.findByCode(countryCode)
.ifPresentOrElse(country -> {
expenseReport.setCountry(country);
},
() -> {
throw new InvalidDataException(Country.class, "countryCode", countryCode);
});
}
for (ExpenseItem expenseItem : expenseReport.getExpenses()) {
if (expenseItem.getCurrency() != null && expenseItem.getCurrency().getCode() != null) {
String currencyCode = expenseItem.getCurrency().getCode();
currencyRepository.findByCode(currencyCode)
.ifPresentOrElse(currency -> {
expenseItem.setCurrency(currency);
},
() -> {
throw new InvalidDataException(Currency.class, "currencyCode", currencyCode);
});
}
if (expenseItem.getExpenseCity() != null && expenseItem.getExpenseCity().getCode() != null) {
String expenseCityCode = expenseItem.getExpenseCity().getCode();
cityRepository.findByCode(expenseCityCode)
.ifPresentOrElse(city -> {
expenseItem.setExpenseCity(city);
},
() -> {
throw new InvalidDataException(City.class, "expenseCityCode", expenseCityCode);
});
}
if (expenseItem.getCategory() != null && expenseItem.getCategory().getCode() != null) {
String categoryCode = expenseItem.getCategory().getCode();
categoryRepository.findByCode(categoryCode)
.ifPresentOrElse(expenseCategory -> {
expenseItem.setCategory(expenseCategory);
},
() -> {
throw new InvalidDataException(ExpenseCategory.class, "expenseCategoryCode", categoryCode);
});
}
}
return expenseReportRepository.save(expenseReport);
}
My question is, is this the best way to do this? I feel that if the object gets too complex, I'll have to create this super huge save method.
Does JPA offer a better solution to this? I was thinking also to change the parameterized types (like country, city, state) to use the code itself as it's primary key, rather than an auto-generated id.
Regards.
I am trying to implement MVI Architecture in Android, but don't want to use Mosby Library. I want to learn the basics first.
I am building a sample app where when I press a button, text in the textview changes(initially the text is something else). Here is the code for MainActivity and MainPresenter.
class MainActivity : AppCompatActivity(), MainContract.View {
lateinit var mPresenter: MainContract.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mPresenter = MainPresenter()
mPresenter.attachPresenter(this)
bind()
}
#SuppressLint("CheckResult")
private fun bind() {
mPresenter.states().subscribe({ state ->
render(state)
}, {
Log.e("error", "Error is: ", it)
it.printStackTrace()
})
mPresenter.addIntents(intents())
}
override fun intents(): Observable<MainIntent> {
return Observable.merge(
initialIntent(),
clickIntent()
)
}
override fun render(state: MainViewState) {
btn_show.isEnabled = state.isEnabledButton
helloWorldTextView.text = state.message
loadingIndicator.visibility = if (state.isLoading) View.VISIBLE else View.GONE
}
private fun initialIntent(): Observable<MainIntent.InitialIntent> = Observable.just(MainIntent.InitialIntent)
private fun clickIntent(): Observable<MainIntent.ClickIntent> {
return btn_show.clicks().map { MainIntent.ClickIntent("Eureka") }
}
}
class MainPresenter : MainContract.Presenter {
private val intentsSubject: PublishSubject<MainIntent> = PublishSubject.create()
override fun states(): Observable<MainViewState> {
return statesObservable
}
private lateinit var view: MainContract.View
override fun attachPresenter(view: MainContract.View) {
this.view = view
}
#SuppressLint("CheckResult")
override fun addIntents(intents: Observable<MainIntent>) {
intents.subscribe(intentsSubject)
}
private val reducer =
BiFunction { previousState: MainViewState, result: MainResult ->
when (result) {
is MainResult.InitialResult.InFlight -> previousState.copy(
isLoading = true,
message = "Initial Result",
isEnabledButton = false
)
is MainResult.InitialResult.Success -> previousState.copy(
isLoading = true,
message = "Initial Success",
isEnabledButton = true
)
is MainResult.InitialResult.Error -> previousState.copy(
isLoading = false,
message = "Error Initially",
isEnabledButton = true
)
is MainResult.ClickedResult.Success -> previousState.copy(
isLoading = false,
message = System.currentTimeMillis().toString(),
isEnabledButton = true
)
is MainResult.ClickedResult.Error -> previousState.copy(
isLoading = false,
message = "Error Clicked",
isEnabledButton = true
)
is MainResult.ClickedResult.InFlight -> previousState.copy(
isLoading = true,
message = "Clicked In Flight",
isEnabledButton = false
)
}
}
private fun actionFromIntent(intent: MainIntent): MainAction {
if (intent is MainIntent.InitialIntent) {
return MainAction.InitialAction
} else if (intent is MainIntent.ClickIntent) {
return MainAction.ClickedAction("Hello")
} else {
return MainAction.InitialAction
}
}
private var actionProcessor: ObservableTransformer<MainAction, MainResult> = ObservableTransformer { actions ->
actions.publish { shared ->
Observable.merge<MainResult>(
shared.ofType(MainAction.InitialAction::class.java).compose(initialActionProcessor),
shared.ofType(MainAction.ClickedAction::class.java).compose(clickActionProcessor)
)
}
}
private val initialActionProcessor =
ObservableTransformer<MainAction.InitialAction, MainResult.InitialResult> { action: Observable<MainAction.InitialAction> ->
action.switchMap {
Observable.just("hello initially")
.map { MainResult.InitialResult.Success(it) }
.cast(MainResult.InitialResult::class.java)
.onErrorReturn { MainResult.InitialResult.Error(it.message!!) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith { MainResult.InitialResult.InFlight }
}
}
private val clickActionProcessor =
ObservableTransformer<MainAction.ClickedAction, MainResult.ClickedResult> { action: Observable<MainAction.ClickedAction> ->
Observable.just("Click").map { message ->
MainResult.ClickedResult.Success(message)
}.cast(MainResult.ClickedResult::class.java)
.onErrorReturn { MainResult.ClickedResult.Error("Error") }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith { MainResult.ClickedResult.InFlight }
}
private val statesObservable: Observable<MainViewState> = compose()
private fun compose(): Observable<MainViewState> {
return intentsSubject
.map {
actionFromIntent(it)
}
.compose(actionProcessor)
.scan(MainViewState.idle(), reducer)
.distinctUntilChanged()
.replay(1)
.autoConnect(0)
}
}
Problem is that only the Inital event is fired and nothing else. The code doesn't respond to clicks, render is called only initially once.
Also, if I remove the startWith{} from the actionProcessors code responds to clicks, but only once. After that, nothing happens.
Does anyone see issue with the code? I have been trying to get my head around this problem for a while now.
My previous reply:
It's not straight answer to your question. But if you implement what's below, you probably won't have the problem you actually asked about and you'll have easier MVI solution.
You probably try to merge https://speakerdeck.com/jakewharton/the-state-of-managing-state-with-rxjava-devoxx-us-2017, http://hannesdorfmann.com/android/mosby3-mvi-1 and https://medium.com/#oldergod/managing-state-with-rxjava-b0798a6c5757 ideas.
Take a look here: https://proandroiddev.com/taming-state-in-android-with-elm-architecture-and-kotlin-part-1-566caae0f706 - it's simpler. Part 1 and 2 should be enough.
I tried the 1st approach and was repulsed by initial complexity. In 2nd approach you don't have Action, Intent, Result, but Msg instead. It's simpler to reason about.
There's also new MVI course - but haven't checked it yet.
Current approach:
I tried mentioned Elm Architecture, but it is not complete. There are at least 2 problems:
Only one request can get through queue at one moment. Some RxJava
should do the trick (groupBy with 2 streams: ui, background
probably).
parallel requests would update the same state, so you should differentiate DataStates inside your UiState. So different state for different part of UI.
Before writing actual fixes we realised, this is not the way to go ATM: announced Composables could do the MVI trick: smooth and precise data transition to specific parts of UI.
Disclaimer: moderator removed my answer which WAS actual answer. Even more, my answer moved to comment is cut down, which makes it look unfinished. That's why this post emerged once again. After you read it dear moderator, you can remove disclaimer, thanks :)
We are working for internationalizing an old application with some dirty code. For example, we have an object DTO InstrumentDto:
private String label;
private Quotation quotation;
private ExprQuote quoteExp;
public String getTypeCouponCouru() {
if (this.quoteExp.getCode().equals(Constants.INS_QUOTE_EXPR_PCT_NOMINAL_CPN_INCLUS)
|| this.quoteExp.getCode().equals(Constants.INS_QUOTE_EXPR_PCT_NOMINAL_INTERET)) {
return "Coupon attaché";
} else if(this.quoteExp.getCode().equals(Constants.INS_QUOTE_EXPR_PCT_NOMINAL_PIED_CPN)){
return "Coupon détaché";
} else {
return "";
}
}
public String getFormattedLabel() {
StringBuilder formattedLabel = new StringBuilder(this.label);
Quotation quote = this.quotation;
if (this.quotation != null) {
formattedLabel.append(" ");
formattedLabel.append(FormatUtil.formatDecimal(this.quotation.getCryQuote());
if (this.quoteExp.getType().equals("PERCENT")) {
formattedLabel.append(" %");
} else {
formattedLabel.append(" ");
formattedLabel.append(this.quotation.getCurrency().getCode());
}
formattedLabel.append(" le ");
formattedLabel.append(DateUtil.formatDate(this.quotation.getValoDate()));
}
return formattedLabel.toString();
}
Then, those methods are used on JSPs. For example for getFormattedLabel(), we have :
<s:select name = "orderUnitaryForm.fieldInstrument"
id = "fieldInstrument"
list = "orderUnitaryForm.instrumentList"
listKey = "id"
listValue = "formattedLabel" />
IMO, the first method doesn't have its place on the DTO. We are expecting the view to manage the label to print. And in this view (the JSP), no problem to translate those words.
Additionally, this method is just used in 2 JSP. Not a problem to "repeat" the conditional tests.
But it's more difficult for getFormattedLabel() : this method is used in a lot of JSP, and the building of the formatted label is "complicated". And it's not possible having the i18n service in the DTO.
So how to do that ?
Your code in getFormattedLabel() seems to be business logic.
A DTO is a simple object without any complex test/behavior (see wiki definition).
IMO, you should move this chunk of code to your Action and split your *.properties file like this:
Your *.properties:
message1= {0} % le {1}
message2= {0} {1} le {2}
Your Action:
public MyAction extends ActionSupport {
public String execute(){
//useful code here
InstrumentDto dto = new InstrumentDto();
StringBuilder formattedLabel = new StringBuilder(label);
if (this.quotation != null) {
String cryQuote = FormatUtil.formatDecimal(this.quotation.getCryQuote());
String date = DateUtil.formatDate(this.quotation.getValoDate());
if (this.quoteExp.getType().equals("PERCENT")) {
formattedLabel.append(getText("message1", new String[] { cryQuote, date }));
} else {
String cryCode = this.quotation.getCurrency().getCode();
formattedLabel.append(getText("message2", new String[] { cryQuote, cryCode, date }));
}
}
dto.setFormattedLabel(formattedLabel);
}
}
Hope this will help ;)