• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FIWARE / contract-management / #87

16 Apr 2026 07:39AM UTC coverage: 1.681%. Remained the same
#87

Pull #22

web-flow
Merge branch 'main' into move-check
Pull Request #22: move check

0 of 2 new or added lines in 1 file covered. (0.0%)

587 of 34920 relevant lines covered (1.68%)

0.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/java/org/fiware/iam/dsp/RainbowProductOfferingHandler.java
1
package org.fiware.iam.dsp;
2

3
import io.micronaut.context.annotation.Requires;
4
import io.micronaut.http.HttpResponse;
5
import io.micronaut.http.HttpStatus;
6
import jakarta.inject.Singleton;
7
import lombok.RequiredArgsConstructor;
8
import lombok.extern.slf4j.Slf4j;
9
import org.fiware.iam.configuration.GeneralProperties;
10
import org.fiware.iam.handlers.ProductOfferingHandler;
11
import org.fiware.iam.tmforum.ProductOfferingConstants;
12
import org.fiware.iam.tmforum.productcatalog.api.ProductSpecificationApiClient;
13
import org.fiware.iam.tmforum.productcatalog.model.*;
14
import org.fiware.rainbow.api.CatalogApiClient;
15
import org.fiware.rainbow.model.CatalogVO;
16
import org.fiware.rainbow.model.DataServiceVO;
17
import org.fiware.rainbow.model.NewDataserviceVO;
18
import reactor.core.publisher.Mono;
19

20
import java.util.ArrayList;
21
import java.util.Arrays;
22
import java.util.List;
23
import java.util.Optional;
24
import java.util.function.Function;
25

26

27
/**
28
 * Handler to manage product offerings and exchange them with Rainbow.
29
 */
30
@Requires(condition = GeneralProperties.RainbowCondition.class)
31
@RequiredArgsConstructor
32
@Singleton
33
@Slf4j
×
34
public class RainbowProductOfferingHandler implements ProductOfferingHandler {
35

36
    private static final String EMPTY_STRING_MARKER = "Empty";
37
    private static final String OWNER_ROLE = "Owner";
38

39
    private final CatalogApiClient rainbowCatalogApiClient;
40
    private final org.fiware.iam.tmforum.productcatalog.api.CatalogApiClient catalogApiClient;
41
    private final ProductSpecificationApiClient productSpecificationApiClient;
42

43
    @Override
44
    public Mono<HttpResponse<?>> handleOfferingCreation(ProductOfferingVO productOfferingVO) {
45

NEW
46
        if (productOfferingVO.getCategory() == null || productOfferingVO.getCategory().isEmpty()) {
×
NEW
47
            throw new IllegalArgumentException("Product offering does not have a category.");
×
48
        }
49
        Mono<NewDataserviceVO> dataserviceVOMono = prepareNewDataservice(productOfferingVO);
×
50
        Mono<List<String>> catalogsMono = getCatalogsForProductOffering(productOfferingVO);
×
51
        return Mono.zip(dataserviceVOMono, catalogsMono, this::createDataservice)
×
52
                        .flatMap(Function.identity());
×
53
    }
54

55
    @Override
56
    public Mono<HttpResponse<?>> handleOfferingStateChange(ProductOfferingVO productOfferingVO) {
57
        // find catalogs that the offering should be in
58
        Mono<List<String>> targetCatalogs = getCatalogsForProductOffering(productOfferingVO);
×
59

60
        // (rainbow) catalogs that the offering is currently included
61
        Mono<List<String>> currentCatalogs = rainbowCatalogApiClient.getCatalogs()
×
62
                .map(HttpResponse::body)
×
63
                .map(catalogVOS -> catalogVOS.stream().map(CatalogVO::getAtId).toList());
×
64

65
        return Mono.zipDelayError(targetCatalogs, currentCatalogs)
×
66
                .flatMap(tuple -> handleCatalogEntries(tuple.getT1(), tuple.getT2(), productOfferingVO));
×
67
    }
68

69
    @Override
70
    public Mono<HttpResponse<?>> handleOfferingDeletion(ProductOfferingVO productOfferingVO) {
71
        return rainbowCatalogApiClient.getCatalogs()
×
72
                .map(HttpResponse::body)
×
73
                .flatMap(catalogVOS ->
×
74
                        Mono.zipDelayError(
×
75
                                catalogVOS.stream()
×
76
                                        .filter(catalogVO ->
×
77
                                                catalogVO.getDcatColonService()
×
78
                                                        .stream()
×
79
                                                        .map(DataServiceVO::getAtId)
×
80
                                                        .anyMatch(id -> id.equals(productOfferingVO.getId()))
×
81
                                        )
82
                                        .map(catalogVO -> rainbowCatalogApiClient
×
83
                                                .deleteDataserviceInCatalog(catalogVO.getAtId(), productOfferingVO.getId())
×
84

85
                                        ).toList(),
×
86
                                responses -> HttpResponse.noContent())
×
87
                );
88
    }
89

90
    private Optional<String> getCharValue(List<CharacteristicValueSpecificationVO> specs) {
91
        if (specs == null || specs.isEmpty()) {
×
92
            return Optional.empty();
×
93
        }
94
        if (specs.size() == 1 && specs.get(0).getValue() instanceof String stringValue) {
×
95
            return Optional.of(stringValue);
×
96
        }
97
        return specs.stream()
×
98
                .filter(cvs -> Optional.ofNullable(cvs.getIsDefault()).orElse(false))
×
99
                .findFirst()
×
100
                .map(CharacteristicValueSpecificationVO::getValue)
×
101
                .filter(String.class::isInstance)
×
102
                .map(String.class::cast);
×
103
    }
104

105
    private void setEndpoint(ProductSpecificationVO spec, NewDataserviceVO newDataserviceVO) {
106
        if (spec.getProductSpecCharacteristic() != null && !spec.getProductSpecCharacteristic().isEmpty()) {
×
107
            spec.getProductSpecCharacteristic().forEach(psc -> {
×
108
                if (psc.getValueType().equals(ProductOfferingConstants.ENDPOINT_URL_TYPE)) {
×
109
                    getCharValue(psc.getProductSpecCharacteristicValue())
×
110
                            .ifPresent(newDataserviceVO::dcatColonEndpointURL);
×
111

112
                } else if (psc.getValueType().equals(ProductOfferingConstants.ENDPOINT_DESCRIPTION_TYPE)) {
×
113
                    getCharValue(psc.getProductSpecCharacteristicValue())
×
114
                            .ifPresent(newDataserviceVO::dcatColonEndpointDescription);
×
115
                }
116
            });
×
117
        }
118
    }
×
119

120
    private void setRelatedParty(ProductSpecificationVO spec, NewDataserviceVO newDataserviceVO) {
121
        if (spec.getRelatedParty() != null && !spec.getRelatedParty().isEmpty()) {
×
122
            spec.getRelatedParty().stream()
×
123
                    .filter(rp -> rp.getRole().equals(OWNER_ROLE))
×
124
                    .map(RelatedPartyVO::getId)
×
125
                    .findFirst()
×
126
                    .ifPresent(newDataserviceVO::dctColonCreator);
×
127
        }
128
    }
×
129

130
    private Mono<HttpResponse<?>> handleCatalogEntries(List<String> targetCatalogs, List<String> currentCatalogs, ProductOfferingVO productOfferingVO) {
131
        List<String> newCatalogs = new ArrayList<>();
×
132
        List<String> updateCatalogs = new ArrayList<>();
×
133
        targetCatalogs.stream().forEach(targetCatalog -> {
×
134
            if (currentCatalogs.contains(targetCatalog)) {
×
135
                updateCatalogs.add(targetCatalog);
×
136
            } else {
137
                newCatalogs.add(targetCatalog);
×
138
            }
139
        });
×
140
        List<String> deleteCatalogs = currentCatalogs.stream().filter(currentCatalog -> !targetCatalogs.contains(currentCatalog)).toList();
×
141

142
        List<Mono<HttpResponse<?>>> offeringMonos = new ArrayList<>();
×
143
        offeringMonos.add(
×
144
                Mono.zipDelayError(deleteCatalogs.stream()
×
145
                        .map(catalogId -> rainbowCatalogApiClient.deleteDataserviceInCatalog(catalogId, productOfferingVO.getId()))
×
146
                        .toList(), r -> HttpResponse.accepted()));
×
147

148
        if (!newCatalogs.isEmpty() || !updateCatalogs.isEmpty()) {
×
149
            Mono<NewDataserviceVO> newDataservice = prepareNewDataservice(productOfferingVO);
×
150
            offeringMonos.add(newDataservice.flatMap(dataserviceVO -> {
×
151
                List<Mono<HttpResponse<?>>> rainbowResponses = new ArrayList<>();
×
152
                if (!newCatalogs.isEmpty()) {
×
153
                    rainbowResponses.add(createDataservice(dataserviceVO, newCatalogs));
×
154
                }
155
                if (!updateCatalogs.isEmpty()) {
×
156
                    rainbowResponses.add(updateDataservice(dataserviceVO, updateCatalogs));
×
157
                }
158
                return Mono.zipDelayError(rainbowResponses, r -> HttpResponse.accepted());
×
159
            }));
160
        }
161
        return Mono.zipDelayError(offeringMonos, r -> HttpResponse.accepted());
×
162
    }
163

164
    private Mono<HttpResponse<?>> updateDataservice(NewDataserviceVO dataserviceVO, List<String> catalogs) {
165
        return Mono.zip(catalogs.stream()
×
166
                        .map(id -> rainbowCatalogApiClient
×
167
                                .updateDataserviceInCatalog(id, dataserviceVO.getAtId(), dataserviceVO)
×
168
                                .onErrorMap(t ->
×
169
                                        new IllegalArgumentException("Was not able to update dataservice %s in %s".formatted(dataserviceVO, id), t)))
×
170
                        .toList(),
×
171
                responses -> Arrays.stream(responses)
×
172
                        .map(HttpResponse.class::cast)
×
173
                        .filter(resp -> resp.getStatus() != HttpStatus.ACCEPTED)
×
174
                        .findAny()
×
175
                        .map(r -> HttpResponse.status(HttpStatus.BAD_GATEWAY))
×
176
                        .orElse(HttpResponse.ok()));
×
177

178
    }
179

180
    private Mono<NewDataserviceVO> prepareNewDataservice(ProductOfferingVO productOfferingVO) {
181
        NewDataserviceVO newDataserviceVO = new NewDataserviceVO().atId(productOfferingVO.getId());
×
182
        return getSpecForOffering(productOfferingVO)
×
183
                .map(spec -> {
×
184
                    newDataserviceVO.dctColonTitle(spec.getName());
×
185
                    setRelatedParty(spec, newDataserviceVO);
×
186
                    setEndpoint(spec, newDataserviceVO);
×
187
                    return newDataserviceVO;
×
188
                });
189
    }
190

191
    private Mono<ProductSpecificationVO> getSpecForOffering(ProductOfferingVO productOfferingVO) {
192
        return productSpecificationApiClient.retrieveProductSpecification(productOfferingVO.getProductSpecification().getId(), null)
×
193
                .map(HttpResponse::body);
×
194
    }
195

196
    private Mono<List<String>> getCatalogsForProductOffering(ProductOfferingVO productOfferingVO) {
197
        List<String> categoryIds = productOfferingVO.getCategory().stream().map(CategoryRefVO::getId).toList();
×
198

199
        return rainbowCatalogApiClient.getCatalogs()
×
200
                .map(HttpResponse::body)
×
201
                .flatMap(catalogList ->
×
202
                        Mono.zip(catalogList.stream()
×
203
                                        .map(CatalogVO::getAtId)
×
204
                                        .map(id -> catalogApiClient.retrieveCatalog(id, null)
×
205
                                                .map(HttpResponse::body)
×
206
                                                .filter(cvo -> cvo.getCategory().stream()
×
207
                                                        .map(CategoryRefVO::getId)
×
208
                                                        .anyMatch(categoryIds::contains)
×
209
                                                )
210
                                                .map(org.fiware.iam.tmforum.productcatalog.model.CatalogVO::getId)
×
211
                                                // prevent empty monos, because they will terminate the zip
212
                                                .defaultIfEmpty(EMPTY_STRING_MARKER)
×
213
                                        )
214
                                        .toList(),
×
215
                                ids -> Arrays.stream(ids)
×
216
                                        .filter(String.class::isInstance)
×
217
                                        .map(String.class::cast)
×
218
                                        .filter(id -> !id.equals(EMPTY_STRING_MARKER))
×
219
                                        .toList())
×
220
                );
221
    }
222

223

224
    private Mono<HttpResponse<?>> createDataservice(NewDataserviceVO dataserviceVO, List<String> catalogs) {
225
        return Mono.zip(catalogs.stream()
×
226
                        .map(id -> rainbowCatalogApiClient
×
227
                                .createDataserviceInCatalog(id, dataserviceVO)
×
228
                                .onErrorMap(t ->
×
229
                                        new IllegalArgumentException("Was not able to create dataservice %s".formatted(dataserviceVO), t)))
×
230
                        .toList(),
×
231
                responses -> Arrays.stream(responses)
×
232
                        .map(HttpResponse.class::cast)
×
233
                        .filter(resp -> resp.getStatus() != HttpStatus.CREATED)
×
234
                        .findAny()
×
235
                        .map(r -> HttpResponse.status(HttpStatus.BAD_GATEWAY))
×
236
                        .orElse(HttpResponse.ok()));
×
237

238
    }
239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc