diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/metadata/RemoteScheduleFetcher.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/metadata/RemoteScheduleFetcher.java index 248df2a60f6c..ea1be39fc5bd 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/metadata/RemoteScheduleFetcher.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/metadata/RemoteScheduleFetcher.java @@ -21,6 +21,7 @@ import com.google.gson.GsonBuilder; import com.google.inject.Inject; import io.cdap.cdap.api.schedule.Trigger; +import io.cdap.cdap.client.ScheduleClient; import io.cdap.cdap.common.NotFoundException; import io.cdap.cdap.common.ProgramNotFoundException; import io.cdap.cdap.common.conf.Constants; @@ -73,7 +74,8 @@ public ScheduleDetail get(ScheduleId scheduleId) throws IOException, ScheduleNotFoundException, UnauthorizedException { String url = String.format( "namespaces/%s/apps/%s/schedules/%s", - scheduleId.getNamespace(), scheduleId.getApplication(), scheduleId.getSchedule()); + scheduleId.getNamespace(), scheduleId.getApplication(), + ScheduleClient.getEncodedScheduleName(scheduleId.getSchedule())); HttpRequest.Builder requestBuilder = remoteClient.requestBuilder(HttpMethod.GET, url); HttpResponse httpResponse; try { diff --git a/cdap-client-tests/src/test/java/io/cdap/cdap/client/ScheduleClientTestRun.java b/cdap-client-tests/src/test/java/io/cdap/cdap/client/ScheduleClientTestRun.java index 8cca45c91710..7b3d8b97b42f 100644 --- a/cdap-client-tests/src/test/java/io/cdap/cdap/client/ScheduleClientTestRun.java +++ b/cdap-client-tests/src/test/java/io/cdap/cdap/client/ScheduleClientTestRun.java @@ -29,6 +29,8 @@ import io.cdap.cdap.proto.id.WorkflowId; import io.cdap.cdap.test.XSlowTests; import java.io.File; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -36,6 +38,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,17 +47,31 @@ * Tests for {@link io.cdap.cdap.client.ServiceClient}. */ @Category(XSlowTests.class) +@RunWith(Parameterized.class) public class ScheduleClientTestRun extends ClientTestBase { private static final Logger LOG = LoggerFactory.getLogger(ScheduleClientTestRun.class); private final NamespaceId namespace = NamespaceId.DEFAULT; private final ApplicationId app = namespace.app(FakeApp.NAME); private final WorkflowId workflow = app.workflow(FakeWorkflow.NAME); - private final ScheduleId schedule = app.schedule(FakeApp.TIME_SCHEDULE_NAME); + private final ScheduleId schedule; private ScheduleClient scheduleClient; private ApplicationClient appClient; + public ScheduleClientTestRun(String scheduleName) { + this.schedule = app.schedule(scheduleName); + } + + @Parameterized.Parameters(name = "{index}: scheduleName = {0}") + public static Collection data() { + Collection params = new ArrayList<>(); + params.add(new String[] { "someSchedule" }); + params.add(new String[] { "some +-:?'` Schedule" }); + params.add(new String[] { "No.10 - 0014002 AND No.16 0015006" }); + return params; + } + @Before public void setUp() throws Throwable { super.setUp(); @@ -73,13 +91,17 @@ public void tearDown() throws Throwable { @Test public void testAll() throws Exception { + File appJar = createAppJarFile(FakeApp.class); + // deploy the app with time schedule + FakeApp.AppConfig config = new FakeApp.AppConfig(true, schedule.getSchedule(), null); + appClient.deploy(namespace, appJar, config); List list = scheduleClient.listSchedules(workflow); Assert.assertEquals(1, list.size()); ScheduleDetail timeSchedule = list.get(0); ProtoTrigger.TimeTrigger timeTrigger = (ProtoTrigger.TimeTrigger) timeSchedule.getTrigger(); - Assert.assertEquals(FakeApp.TIME_SCHEDULE_NAME, timeSchedule.getName()); + Assert.assertEquals(schedule.getSchedule(), timeSchedule.getName()); Assert.assertEquals(FakeApp.SCHEDULE_CRON, timeTrigger.getCronExpression()); @@ -135,14 +157,14 @@ public void testScheduleChanges() throws Exception { File appJar = createAppJarFile(FakeApp.class); // deploy the app with time schedule - FakeApp.AppConfig config = new FakeApp.AppConfig(true, null, null); + FakeApp.AppConfig config = new FakeApp.AppConfig(true, schedule.getSchedule(), null); appClient.deploy(namespace, appJar, config); // now there should be one schedule List list = scheduleClient.listSchedules(workflow); Assert.assertEquals(1, list.size()); // test updating the schedule cron - config = new FakeApp.AppConfig(true, FakeApp.TIME_SCHEDULE_NAME, "0 2 1 1 *"); + config = new FakeApp.AppConfig(true, schedule.getSchedule(), "0 2 1 1 *"); appClient.deploy(namespace, appJar, config); list = scheduleClient.listSchedules(workflow); Assert.assertEquals(1, list.size()); diff --git a/cdap-client/src/main/java/io/cdap/cdap/client/ApplicationClient.java b/cdap-client/src/main/java/io/cdap/cdap/client/ApplicationClient.java index eeec71e5c50b..74cd061fe5b9 100644 --- a/cdap-client/src/main/java/io/cdap/cdap/client/ApplicationClient.java +++ b/cdap-client/src/main/java/io/cdap/cdap/client/ApplicationClient.java @@ -704,7 +704,7 @@ public void addSchedule(ApplicationId app, ScheduleDetail scheduleDetail) UnauthorizedException, BadRequestException { String path = String.format("apps/%s/versions/%s/schedules/%s", app.getApplication(), app.getVersion(), - scheduleDetail.getName()); + ScheduleClient.getEncodedScheduleName(scheduleDetail.getName())); HttpResponse response = restClient.execute(HttpMethod.PUT, config.resolveNamespacedURLV3(app.getParent(), path), GSON.toJson(scheduleDetail), ImmutableMap.of(), diff --git a/cdap-client/src/main/java/io/cdap/cdap/client/ScheduleClient.java b/cdap-client/src/main/java/io/cdap/cdap/client/ScheduleClient.java index 57648a97c75a..db8ed2a51066 100644 --- a/cdap-client/src/main/java/io/cdap/cdap/client/ScheduleClient.java +++ b/cdap-client/src/main/java/io/cdap/cdap/client/ScheduleClient.java @@ -41,9 +41,12 @@ import io.cdap.common.http.HttpResponse; import io.cdap.common.http.ObjectResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -134,7 +137,7 @@ public void suspend(ScheduleId scheduleId) throws IOException, UnauthenticatedException, NotFoundException, UnauthorizedException { String path = String.format("apps/%s/schedules/%s/suspend", scheduleId.getApplication(), - scheduleId.getSchedule()); + getEncodedScheduleName(scheduleId.getSchedule())); URL url = config.resolveNamespacedURLV3(scheduleId.getNamespaceId(), path); HttpResponse response = restClient.execute(HttpMethod.POST, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); @@ -147,7 +150,7 @@ public void resume(ScheduleId scheduleId) throws IOException, UnauthenticatedException, NotFoundException, UnauthorizedException { String path = String.format("apps/%s/schedules/%s/resume", scheduleId.getApplication(), - scheduleId.getSchedule()); + getEncodedScheduleName(scheduleId.getSchedule())); URL url = config.resolveNamespacedURLV3(scheduleId.getNamespaceId(), path); HttpResponse response = restClient.execute(HttpMethod.POST, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); @@ -165,7 +168,7 @@ public void delete(ScheduleId scheduleId) throws IOException, UnauthenticatedException, NotFoundException, UnauthorizedException { String path = String.format("apps/%s/schedules/%s", scheduleId.getApplication(), - scheduleId.getSchedule()); + getEncodedScheduleName(scheduleId.getSchedule())); URL url = config.resolveNamespacedURLV3(scheduleId.getNamespaceId(), path); HttpResponse response = restClient.execute(HttpMethod.DELETE, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); @@ -178,7 +181,7 @@ public String getStatus(ScheduleId scheduleId) throws IOException, UnauthenticatedException, NotFoundException, UnauthorizedException { String path = String.format("apps/%s/schedules/%s/status", scheduleId.getApplication(), - scheduleId.getSchedule()); + getEncodedScheduleName(scheduleId.getSchedule())); URL url = config.resolveNamespacedURLV3(scheduleId.getParent().getParent(), path); HttpResponse response = restClient.execute(HttpMethod.GET, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); @@ -219,7 +222,7 @@ private void doAdd(ScheduleId scheduleId, String json) throws IOException, UnauthenticatedException, NotFoundException, UnauthorizedException, AlreadyExistsException { String path = String.format("apps/%s/schedules/%s", scheduleId.getApplication(), - scheduleId.getSchedule()); + getEncodedScheduleName(scheduleId.getSchedule())); URL url = config.resolveNamespacedURLV3(scheduleId.getNamespaceId(), path); HttpRequest request = HttpRequest.put(url).withBody(json).build(); HttpResponse response = restClient.execute(request, config.getAccessToken(), @@ -236,7 +239,7 @@ private void doUpdate(ScheduleId scheduleId, String json) throws IOException, UnauthenticatedException, NotFoundException, UnauthorizedException, AlreadyExistsException { String path = String.format("apps/%s/schedules/%s/update", scheduleId.getApplication(), - scheduleId.getSchedule()); + getEncodedScheduleName(scheduleId.getSchedule())); URL url = config.resolveNamespacedURLV3(scheduleId.getNamespaceId(), path); HttpRequest request = HttpRequest.post(url).withBody(json).build(); HttpResponse response = restClient.execute(request, config.getAccessToken(), @@ -263,4 +266,8 @@ private List doList(WorkflowId workflow) return objectResponse.getResponseObject(); } + public static String getEncodedScheduleName(String scheduleName) + throws UnsupportedEncodingException { + return URLEncoder.encode(scheduleName, StandardCharsets.UTF_8.toString()).replace("+", "%20"); + } }