264 changed files with 2852 additions and 1287 deletions
@ -1,37 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2023 The Thingsboard Authors |
|
||||
* |
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
* you may not use this file except in compliance with the License. |
|
||||
* You may obtain a copy of the License at |
|
||||
* |
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
* |
|
||||
* Unless required by applicable law or agreed to in writing, software |
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
* See the License for the specific language governing permissions and |
|
||||
* limitations under the License. |
|
||||
*/ |
|
||||
package org.thingsboard.server.service.apiusage.limits; |
|
||||
|
|
||||
import lombok.RequiredArgsConstructor; |
|
||||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
|
||||
|
|
||||
import java.util.function.Function; |
|
||||
|
|
||||
@RequiredArgsConstructor |
|
||||
public enum LimitedApi { |
|
||||
|
|
||||
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), |
|
||||
ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), |
|
||||
NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit), |
|
||||
NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit); |
|
||||
|
|
||||
private final Function<DefaultTenantProfileConfiguration, String> configExtractor; |
|
||||
|
|
||||
public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) { |
|
||||
return configExtractor.apply(profileConfiguration); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,110 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.limits; |
||||
|
|
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.mockito.Mockito; |
||||
|
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
import org.thingsboard.server.common.data.TenantProfile; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.NotificationRuleId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; |
||||
|
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; |
||||
|
import org.thingsboard.server.dao.util.limits.DefaultRateLimitService; |
||||
|
import org.thingsboard.server.dao.util.limits.LimitedApi; |
||||
|
import org.thingsboard.server.dao.util.limits.RateLimitService; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.junit.Assert.assertFalse; |
||||
|
import static org.junit.Assert.assertTrue; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.reset; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@RunWith(MockitoJUnitRunner.class) |
||||
|
public class RateLimitServiceTest { |
||||
|
|
||||
|
private RateLimitService rateLimitService; |
||||
|
private TbTenantProfileCache tenantProfileCache; |
||||
|
private TenantId tenantId; |
||||
|
|
||||
|
@Before |
||||
|
public void beforeEach() { |
||||
|
tenantProfileCache = Mockito.mock(TbTenantProfileCache.class); |
||||
|
rateLimitService = new DefaultRateLimitService(tenantProfileCache, 60, 100); |
||||
|
tenantId = new TenantId(UUID.randomUUID()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testRateLimits() { |
||||
|
int max = 2; |
||||
|
String rateLimit = max + ":600"; |
||||
|
DefaultTenantProfileConfiguration profileConfiguration = new DefaultTenantProfileConfiguration(); |
||||
|
profileConfiguration.setTenantEntityExportRateLimit(rateLimit); |
||||
|
profileConfiguration.setTenantEntityImportRateLimit(rateLimit); |
||||
|
profileConfiguration.setTenantNotificationRequestsRateLimit(rateLimit); |
||||
|
profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(rateLimit); |
||||
|
profileConfiguration.setTenantServerRestLimitsConfiguration(rateLimit); |
||||
|
profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit); |
||||
|
profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit); |
||||
|
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); |
||||
|
updateTenantProfileConfiguration(profileConfiguration); |
||||
|
|
||||
|
for (LimitedApi limitedApi : List.of( |
||||
|
LimitedApi.ENTITY_EXPORT, |
||||
|
LimitedApi.ENTITY_IMPORT, |
||||
|
LimitedApi.NOTIFICATION_REQUESTS, |
||||
|
LimitedApi.REST_REQUESTS, |
||||
|
LimitedApi.CASSANDRA_QUERIES |
||||
|
)) { |
||||
|
testRateLimits(limitedApi, max, tenantId); |
||||
|
} |
||||
|
|
||||
|
CustomerId customerId = new CustomerId(UUID.randomUUID()); |
||||
|
testRateLimits(LimitedApi.REST_REQUESTS, max, customerId); |
||||
|
|
||||
|
NotificationRuleId notificationRuleId = new NotificationRuleId(UUID.randomUUID()); |
||||
|
testRateLimits(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, max, notificationRuleId); |
||||
|
|
||||
|
String wsSessionId = UUID.randomUUID().toString(); |
||||
|
testRateLimits(LimitedApi.WS_UPDATES_PER_SESSION, max, wsSessionId); |
||||
|
} |
||||
|
|
||||
|
private void testRateLimits(LimitedApi limitedApi, int max, Object level) { |
||||
|
for (int i = 1; i <= max; i++) { |
||||
|
boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); |
||||
|
assertTrue(success); |
||||
|
} |
||||
|
boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); |
||||
|
assertFalse(success); |
||||
|
} |
||||
|
|
||||
|
private void updateTenantProfileConfiguration(DefaultTenantProfileConfiguration profileConfiguration) { |
||||
|
reset(tenantProfileCache); |
||||
|
TenantProfile tenantProfile = new TenantProfile(); |
||||
|
TenantProfileData profileData = new TenantProfileData(); |
||||
|
profileData.setConfiguration(profileConfiguration); |
||||
|
tenantProfile.setProfileData(profileData); |
||||
|
when(tenantProfileCache.get(eq(tenantId))).thenReturn(tenantProfile); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.security.auth.oauth2; |
||||
|
|
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.mockito.Mockito; |
||||
|
|
||||
|
import javax.servlet.http.Cookie; |
||||
|
import javax.servlet.http.HttpServletRequest; |
||||
|
import java.io.IOException; |
||||
|
import java.io.ObjectInputStream; |
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME; |
||||
|
|
||||
|
public class HttpCookieOAuth2AuthorizationRequestRepositoryTest { |
||||
|
|
||||
|
private static final String SERIALIZED_ATTACK_STRING = |
||||
|
"rO0ABXNyAHVvcmcudGhpbmdzYm9hcmQuc2VydmVyLnNlcnZpY2Uuc2VjdXJpdHkuYXV0aC5vYXV0aDIuSHR0cENvb2tpZU9BdXRoMkF1dGhvcml6YXRpb25SZXF1ZXN0UmVwb3NpdG9yeVRlc3QkTWFsaWNpb3VzQ2xhc3MAAAAAAAAAAAIAAHhw"; |
||||
|
|
||||
|
private static int maliciousMethodInvocationCounter; |
||||
|
|
||||
|
@Before |
||||
|
public void resetInvocationCounter() { |
||||
|
maliciousMethodInvocationCounter = 0; |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void whenLoadAuthorizationRequest_thenMaliciousMethodNotInvoked() { |
||||
|
HttpCookieOAuth2AuthorizationRequestRepository cookieRequestRepo = new HttpCookieOAuth2AuthorizationRequestRepository(); |
||||
|
HttpServletRequest request = Mockito.mock(HttpServletRequest.class); |
||||
|
Cookie cookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, SERIALIZED_ATTACK_STRING); |
||||
|
Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); |
||||
|
|
||||
|
cookieRequestRepo.loadAuthorizationRequest(request); |
||||
|
|
||||
|
assertEquals(0, maliciousMethodInvocationCounter); |
||||
|
} |
||||
|
|
||||
|
private static class MaliciousClass implements Serializable { |
||||
|
private static final long serialVersionUID = 0L; |
||||
|
|
||||
|
public void maliciousMethod() { |
||||
|
maliciousMethodInvocationCounter++; |
||||
|
} |
||||
|
|
||||
|
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
||||
|
maliciousMethod(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sms; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import org.junit.After; |
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.mock.mockito.SpyBean; |
||||
|
import org.springframework.test.context.TestPropertySource; |
||||
|
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.AdminSettings; |
||||
|
import org.thingsboard.server.common.data.FeaturesInfo; |
||||
|
import org.thingsboard.server.common.data.TenantProfile; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; |
||||
|
import org.thingsboard.server.controller.AbstractControllerTest; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
import org.thingsboard.server.dao.settings.AdminSettingsService; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Optional; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.Mockito.doReturn; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
@TestPropertySource(properties = { |
||||
|
"usage.stats.report.enabled=true", |
||||
|
"usage.stats.report.interval=1", |
||||
|
}) |
||||
|
public class DefaultSmsServiceTest extends AbstractControllerTest { |
||||
|
@SpyBean |
||||
|
private DefaultSmsService defaultSmsService; |
||||
|
@Autowired |
||||
|
private AdminSettingsService adminSettingsService; |
||||
|
|
||||
|
private TenantProfile tenantProfile; |
||||
|
|
||||
|
@Before |
||||
|
public void before() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
prepareSmsSystemSetting(); |
||||
|
} |
||||
|
|
||||
|
@After |
||||
|
public void after() throws Exception { |
||||
|
saveTenantProfileWitConfiguration(tenantProfile, new DefaultTenantProfileConfiguration()); |
||||
|
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms"); |
||||
|
resetTokens(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testLimitSmsMessagingByTenantProfileSettings() throws Exception { |
||||
|
tenantProfile = getDefaultTenantProfile(); |
||||
|
|
||||
|
DefaultTenantProfileConfiguration config = createTenantProfileConfigurationWithSmsLimits(10, true); |
||||
|
saveTenantProfileWitConfiguration(tenantProfile, config); |
||||
|
|
||||
|
for (int i = 0; i < 10; i++) { |
||||
|
doReturn(1).when(defaultSmsService).sendSms(any(), any()); |
||||
|
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message"); |
||||
|
} |
||||
|
|
||||
|
//wait 1 sec so that api usage state is updated
|
||||
|
TimeUnit.SECONDS.sleep(1); |
||||
|
assertThrows(RuntimeException.class, () -> { |
||||
|
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message"); |
||||
|
}, "SMS sending is disabled due to API limits!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testLimitSmsMessagingIfSmsDisabled() throws Exception { |
||||
|
tenantProfile = getDefaultTenantProfile(); |
||||
|
|
||||
|
DefaultTenantProfileConfiguration config = createTenantProfileConfigurationWithSmsLimits(0, false); |
||||
|
saveTenantProfileWitConfiguration(tenantProfile, config); |
||||
|
|
||||
|
TimeUnit.SECONDS.sleep(1); |
||||
|
assertThrows(RuntimeException.class, () -> { |
||||
|
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message"); |
||||
|
}, "SMS sending is disabled due to API limits!"); |
||||
|
|
||||
|
//enable sms messaging
|
||||
|
DefaultTenantProfileConfiguration config2 = createTenantProfileConfigurationWithSmsLimits(0, true); |
||||
|
saveTenantProfileWitConfiguration(tenantProfile, config2); |
||||
|
TimeUnit.SECONDS.sleep(1); |
||||
|
|
||||
|
for (int i = 0; i < 10; i++) { |
||||
|
doReturn(1).when(defaultSmsService).sendSms(any(), any()); |
||||
|
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private TenantProfile getDefaultTenantProfile() throws Exception { |
||||
|
|
||||
|
PageLink pageLink = new PageLink(17); |
||||
|
PageData<TenantProfile> pageData = doGetTypedWithPageLink("/api/tenantProfiles?", |
||||
|
new TypeReference<>(){}, pageLink); |
||||
|
Assert.assertFalse(pageData.hasNext()); |
||||
|
Assert.assertEquals(1, pageData.getTotalElements()); |
||||
|
List<TenantProfile> tenantProfiles = new ArrayList<>(pageData.getData()); |
||||
|
|
||||
|
Optional<TenantProfile> optionalDefaultProfile = tenantProfiles.stream().filter(TenantProfile::isDefault).reduce((a, b) -> null); |
||||
|
Assert.assertTrue(optionalDefaultProfile.isPresent()); |
||||
|
|
||||
|
return optionalDefaultProfile.get(); |
||||
|
} |
||||
|
|
||||
|
private DefaultTenantProfileConfiguration createTenantProfileConfigurationWithSmsLimits(Integer maxSms, Boolean smsEnabled) { |
||||
|
DefaultTenantProfileConfiguration.DefaultTenantProfileConfigurationBuilder builder = DefaultTenantProfileConfiguration.builder(); |
||||
|
builder.maxSms(maxSms); |
||||
|
builder.smsEnabled(smsEnabled); |
||||
|
return builder.build(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private void saveTenantProfileWitConfiguration(TenantProfile tenantProfile, TenantProfileConfiguration tenantProfileConfiguration) { |
||||
|
TenantProfileData tenantProfileData = tenantProfile.getProfileData(); |
||||
|
tenantProfileData.setConfiguration(tenantProfileConfiguration); |
||||
|
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); |
||||
|
Assert.assertNotNull(savedTenantProfile); |
||||
|
} |
||||
|
|
||||
|
private void prepareSmsSystemSetting() throws Exception { |
||||
|
if (doGet("/api/admin/settings/sms").andReturn().getResponse().getStatus() == 404) { |
||||
|
AdminSettings adminSettings = new AdminSettings(); |
||||
|
ObjectNode value = JacksonUtil.newObjectNode(); |
||||
|
value.put("numberFrom", "+12543223870"); |
||||
|
value.put("accountSid", "testAcc"); |
||||
|
value.put("accountToken", "testToken"); |
||||
|
value.put("type", "TWILIO"); |
||||
|
adminSettings.setKey("sms"); |
||||
|
adminSettings.setJsonValue(value); |
||||
|
|
||||
|
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.exception; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
public class TenantProfileNotFoundException extends RuntimeException { |
||||
|
|
||||
|
@Getter |
||||
|
private final TenantId tenantId; |
||||
|
|
||||
|
public TenantProfileNotFoundException(TenantId tenantId) { |
||||
|
super("Profile for tenant with id " + tenantId + " not found"); |
||||
|
this.tenantId = tenantId; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,272 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.util; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.eclipse.leshan.core.LwM2m; |
||||
|
import org.eclipse.leshan.core.model.DDFFileValidator; |
||||
|
import org.eclipse.leshan.core.model.DefaultDDFFileValidator; |
||||
|
import org.eclipse.leshan.core.model.InvalidDDFFileException; |
||||
|
import org.eclipse.leshan.core.model.ObjectModel; |
||||
|
import org.eclipse.leshan.core.model.ResourceModel; |
||||
|
import org.eclipse.leshan.core.util.StringUtils; |
||||
|
import org.w3c.dom.DOMException; |
||||
|
import org.w3c.dom.Document; |
||||
|
import org.w3c.dom.Node; |
||||
|
import org.w3c.dom.NodeList; |
||||
|
import org.xml.sax.SAXException; |
||||
|
|
||||
|
import javax.xml.parsers.DocumentBuilder; |
||||
|
import javax.xml.parsers.DocumentBuilderFactory; |
||||
|
import javax.xml.parsers.ParserConfigurationException; |
||||
|
import java.io.IOException; |
||||
|
import java.io.InputStream; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class TbDDFFileParser { |
||||
|
private static final DDFFileValidator ddfFileValidator = new DefaultDDFFileValidator(); |
||||
|
|
||||
|
public List<ObjectModel> parse(InputStream inputStream, String streamName) |
||||
|
throws InvalidDDFFileException, IOException { |
||||
|
streamName = streamName == null ? "" : streamName; |
||||
|
|
||||
|
log.debug("Parsing DDF file {}", streamName); |
||||
|
|
||||
|
try { |
||||
|
// Parse XML file
|
||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
||||
|
factory.setNamespaceAware(true); |
||||
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); |
||||
|
|
||||
|
DocumentBuilder builder = factory.newDocumentBuilder(); |
||||
|
Document document = builder.parse(inputStream); |
||||
|
|
||||
|
// Get DDF file validator
|
||||
|
LwM2m.LwM2mVersion lwm2mVersion = null; |
||||
|
ddfFileValidator.validate(document); |
||||
|
|
||||
|
// Build list of ObjectModel
|
||||
|
ArrayList<ObjectModel> objects = new ArrayList<>(); |
||||
|
NodeList nodeList = document.getDocumentElement().getElementsByTagName("Object"); |
||||
|
for (int i = 0; i < nodeList.getLength(); i++) { |
||||
|
objects.add(parseObject(nodeList.item(i), streamName, lwm2mVersion, true)); |
||||
|
} |
||||
|
return objects; |
||||
|
} catch (InvalidDDFFileException | SAXException e) { |
||||
|
throw new InvalidDDFFileException(e, "Invalid DDF file %s", streamName); |
||||
|
} |
||||
|
catch (ParserConfigurationException e) { |
||||
|
throw new IllegalStateException("Unable to create Document Builder", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ObjectModel parseObject(Node object, String streamName, LwM2m.LwM2mVersion schemaVersion, boolean validate) |
||||
|
throws InvalidDDFFileException { |
||||
|
|
||||
|
Node objectType = object.getAttributes().getNamedItem("ObjectType"); |
||||
|
if (validate && (objectType == null || !"MODefinition".equals(objectType.getTextContent()))) { |
||||
|
throw new InvalidDDFFileException( |
||||
|
"Object element in %s MUST have a ObjectType attribute equals to 'MODefinition'.", streamName); |
||||
|
} |
||||
|
|
||||
|
Integer id = null; |
||||
|
String name = null; |
||||
|
String description = null; |
||||
|
String version = ObjectModel.DEFAULT_VERSION; |
||||
|
Boolean multiple = null; |
||||
|
Boolean mandatory = null; |
||||
|
Map<Integer, ResourceModel> resources = new HashMap<>(); |
||||
|
String urn = null; |
||||
|
String description2 = null; |
||||
|
String lwm2mVersion = ObjectModel.DEFAULT_VERSION; |
||||
|
|
||||
|
for (int i = 0; i < object.getChildNodes().getLength(); i++) { |
||||
|
Node field = object.getChildNodes().item(i); |
||||
|
if (field.getNodeType() != Node.ELEMENT_NODE) |
||||
|
continue; |
||||
|
|
||||
|
switch (field.getNodeName()) { |
||||
|
case "ObjectID": |
||||
|
id = Integer.valueOf(field.getTextContent()); |
||||
|
break; |
||||
|
case "Name": |
||||
|
name = field.getTextContent(); |
||||
|
break; |
||||
|
case "Description1": |
||||
|
description = field.getTextContent(); |
||||
|
break; |
||||
|
case "ObjectVersion": |
||||
|
if (!StringUtils.isEmpty(field.getTextContent())) { |
||||
|
version = field.getTextContent(); |
||||
|
} |
||||
|
break; |
||||
|
case "MultipleInstances": |
||||
|
if ("Multiple".equals(field.getTextContent())) { |
||||
|
multiple = true; |
||||
|
} else if ("Single".equals(field.getTextContent())) { |
||||
|
multiple = false; |
||||
|
} |
||||
|
break; |
||||
|
case "Mandatory": |
||||
|
if ("Mandatory".equals(field.getTextContent())) { |
||||
|
mandatory = true; |
||||
|
} else if ("Optional".equals(field.getTextContent())) { |
||||
|
mandatory = false; |
||||
|
} |
||||
|
break; |
||||
|
case "Resources": |
||||
|
for (int j = 0; j < field.getChildNodes().getLength(); j++) { |
||||
|
Node item = field.getChildNodes().item(j); |
||||
|
if (item.getNodeType() != Node.ELEMENT_NODE) |
||||
|
continue; |
||||
|
|
||||
|
if (item.getNodeName().equals("Item")) { |
||||
|
ResourceModel resource = parseResource(item, streamName); |
||||
|
if (validate && resources.containsKey(resource.id)) { |
||||
|
throw new InvalidDDFFileException( |
||||
|
"Object %s in %s contains at least 2 resources with same id %s.", |
||||
|
id != null ? id : "", streamName, resource.id); |
||||
|
} else { |
||||
|
resources.put(resource.id, resource); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
case "ObjectURN": |
||||
|
urn = field.getTextContent(); |
||||
|
break; |
||||
|
case "LWM2MVersion": |
||||
|
if (!StringUtils.isEmpty(field.getTextContent())) { |
||||
|
lwm2mVersion = field.getTextContent(); |
||||
|
if (schemaVersion != null && !schemaVersion.toString().equals(lwm2mVersion)) { |
||||
|
throw new InvalidDDFFileException( |
||||
|
"LWM2MVersion is not consistent with xml shema(xsi:noNamespaceSchemaLocation) in %s : %s expected but was %s.", |
||||
|
streamName, schemaVersion, lwm2mVersion); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
case "Description2": |
||||
|
description2 = field.getTextContent(); |
||||
|
break; |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new ObjectModel(id, name, description, version, multiple, mandatory, resources.values(), urn, |
||||
|
lwm2mVersion, description2); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private ResourceModel parseResource(Node item, String streamName) throws DOMException, InvalidDDFFileException { |
||||
|
|
||||
|
Integer id = Integer.valueOf(item.getAttributes().getNamedItem("ID").getTextContent()); |
||||
|
String name = null; |
||||
|
ResourceModel.Operations operations = null; |
||||
|
Boolean multiple = false; |
||||
|
Boolean mandatory = false; |
||||
|
ResourceModel.Type type = null; |
||||
|
String rangeEnumeration = null; |
||||
|
String units = null; |
||||
|
String description = null; |
||||
|
|
||||
|
for (int i = 0; i < item.getChildNodes().getLength(); i++) { |
||||
|
Node field = item.getChildNodes().item(i); |
||||
|
if (field.getNodeType() != Node.ELEMENT_NODE) |
||||
|
continue; |
||||
|
|
||||
|
switch (field.getNodeName()) { |
||||
|
case "Name": |
||||
|
name = field.getTextContent(); |
||||
|
break; |
||||
|
case "Operations": |
||||
|
String strOp = field.getTextContent(); |
||||
|
if (strOp != null && !strOp.isEmpty()) { |
||||
|
operations = ResourceModel.Operations.valueOf(strOp); |
||||
|
} else { |
||||
|
operations = ResourceModel.Operations.NONE; |
||||
|
} |
||||
|
break; |
||||
|
case "MultipleInstances": |
||||
|
if ("Multiple".equals(field.getTextContent())) { |
||||
|
multiple = true; |
||||
|
} else if ("Single".equals(field.getTextContent())) { |
||||
|
multiple = false; |
||||
|
} |
||||
|
break; |
||||
|
case "Mandatory": |
||||
|
if ("Mandatory".equals(field.getTextContent())) { |
||||
|
mandatory = true; |
||||
|
} else if ("Optional".equals(field.getTextContent())) { |
||||
|
mandatory = false; |
||||
|
} |
||||
|
break; |
||||
|
case "Type": |
||||
|
switch (field.getTextContent()) { |
||||
|
case "String": |
||||
|
type = ResourceModel.Type.STRING; |
||||
|
break; |
||||
|
case "Integer": |
||||
|
type = ResourceModel.Type.INTEGER; |
||||
|
break; |
||||
|
case "Float": |
||||
|
type = ResourceModel.Type.FLOAT; |
||||
|
break; |
||||
|
case "Boolean": |
||||
|
type = ResourceModel.Type.BOOLEAN; |
||||
|
break; |
||||
|
case "Opaque": |
||||
|
type = ResourceModel.Type.OPAQUE; |
||||
|
break; |
||||
|
case "Time": |
||||
|
type = ResourceModel.Type.TIME; |
||||
|
break; |
||||
|
case "Objlnk": |
||||
|
type = ResourceModel.Type.OBJLNK; |
||||
|
break; |
||||
|
case "Unsigned Integer": |
||||
|
type = ResourceModel.Type.UNSIGNED_INTEGER; |
||||
|
break; |
||||
|
case "Corelnk": |
||||
|
type = ResourceModel.Type.CORELINK; |
||||
|
break; |
||||
|
case "": |
||||
|
type = ResourceModel.Type.NONE; |
||||
|
break; |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
break; |
||||
|
case "RangeEnumeration": |
||||
|
rangeEnumeration = field.getTextContent(); |
||||
|
break; |
||||
|
case "Units": |
||||
|
units = field.getTextContent(); |
||||
|
break; |
||||
|
case "Description": |
||||
|
description = field.getTextContent(); |
||||
|
break; |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue