44import hudson .model .Descriptor .FormException ;
55import hudson .model .Item ;
66import hudson .model .Job ;
7+ import hudson .model .ParameterDefinition ;
8+ import hudson .model .ParameterValue ;
79import hudson .model .ParametersDefinitionProperty ;
810import hudson .util .FormValidation ;
911import java .time .LocalDateTime ;
1012import java .time .ZonedDateTime ;
1113import java .time .format .DateTimeFormatter ;
1214import java .time .format .DateTimeParseException ;
1315import java .time .temporal .ChronoUnit ;
16+ import java .util .ArrayList ;
17+ import java .util .List ;
1418import java .util .Locale ;
19+ import java .util .UUID ;
1520import java .util .logging .Level ;
1621import java .util .logging .Logger ;
22+ import jenkins .management .Badge ;
23+ import jenkins .model .ParameterizedJobMixIn ;
24+ import jenkins .util .TimeDuration ;
25+ import net .sf .json .JSONArray ;
1726import net .sf .json .JSONObject ;
1827import org .jenkins .ui .icon .IconSpec ;
1928import org .kohsuke .accmod .Restricted ;
2433import org .kohsuke .stapler .QueryParameter ;
2534import org .kohsuke .stapler .StaplerProxy ;
2635import org .kohsuke .stapler .StaplerRequest2 ;
36+ import org .kohsuke .stapler .StaplerResponse2 ;
2737import org .kohsuke .stapler .interceptor .RequirePOST ;
38+ import org .kohsuke .stapler .verb .POST ;
2839
2940public class ScheduleBuildAction implements Action , StaplerProxy , IconSpec {
3041
@@ -130,6 +141,150 @@ public long getQuietPeriodInSeconds() {
130141 return quietperiod ;
131142 }
132143
144+ public Badge getBadge () {
145+ List <ScheduledRun > plannedBuilds = getPlannedBuilds ();
146+ if (plannedBuilds .isEmpty ()) {
147+ return null ;
148+ }
149+ return new Badge (
150+ Integer .toString (plannedBuilds .size ()),
151+ Messages .ScheduleBuildAction_BadgeTooltip (plannedBuilds .size ()),
152+ Badge .Severity .INFO );
153+ }
154+
155+ @ POST
156+ public void doCancelBuild (@ QueryParameter String id , StaplerResponse2 rsp ) {
157+ target .checkPermission (Item .CANCEL );
158+ try {
159+ ScheduledRun toRemove = null ;
160+ for (ScheduledRun sr : ScheduleBuildGlobalConfiguration .get ().getScheduledRuns ()) {
161+ if (sr .getId ().equals (id ) && sr .getJob ().equals (target .getFullName ())) {
162+ toRemove = sr ;
163+ break ;
164+ }
165+ }
166+ if (toRemove != null ) {
167+ ScheduleBuildGlobalConfiguration .get ().getScheduledRuns ().remove (toRemove );
168+ ScheduleBuildGlobalConfiguration .get ().save ();
169+ }
170+ rsp .sendRedirect2 ("./planned" );
171+ } catch (Exception e ) {
172+ throw new RuntimeException (e );
173+ }
174+ }
175+
176+ @ POST
177+ public void doBuild (StaplerRequest2 req , StaplerResponse2 rsp ) {
178+ target .checkPermission (Item .BUILD );
179+ try {
180+ JSONObject formData = req .getSubmittedForm ();
181+ String date = formData .getString ("date" );
182+ boolean scheduleViaCron = formData .has ("scheduleViaCron" );
183+
184+ ZonedDateTime startDateTime , now = ZonedDateTime .now ();
185+
186+ final String time = date .trim ();
187+ try {
188+ startDateTime = parseDateTime (time )
189+ .atZone (ScheduleBuildGlobalConfiguration .get ().getZoneId ());
190+ } catch (DateTimeParseException ex ) {
191+ LOGGER .log (Level .INFO , ex , () -> "Error parsing " + time );
192+ rsp .sendRedirect2 ("error" );
193+ return ;
194+ }
195+
196+ long delay = ChronoUnit .SECONDS .between (now , startDateTime );
197+
198+ if (delay + ScheduleBuildAction .SECURITY_MARGIN < 0 ) { // 120 sec security margin
199+ LOGGER .log (Level .INFO , () -> "Error security margin " + delay );
200+ rsp .sendRedirect2 ("error" );
201+ return ;
202+ }
203+
204+ if (!scheduleViaCron ) {
205+ TimeDuration quietPeriod = new TimeDuration (Math .max (0 , delay ) * 1000 );
206+ new ParameterizedJobMixIn () {
207+ @ Override
208+ protected Job <?, ?> asJob () {
209+ return target ;
210+ }
211+ }.doBuild (req , rsp , quietPeriod );
212+ return ;
213+ }
214+ String id = UUID .randomUUID ().toString ();
215+ List <ParameterValue > values = new ArrayList <>();
216+ JSONObject scheduleViaCronObject = formData .getJSONObject ("scheduleViaCron" );
217+ boolean triggerOnMissed = scheduleViaCronObject .getBoolean ("triggerOnMissed" );
218+
219+ if (target instanceof ParameterizedJobMixIn .ParameterizedJob <?, ?> pj ) {
220+ if (pj .isParameterized ()) {
221+
222+ Object parameter = formData .get ("parameter" );
223+
224+ if (parameter != null ) {
225+ ParametersDefinitionProperty prop = target .getProperty (ParametersDefinitionProperty .class );
226+ JSONArray a = JSONArray .fromObject (parameter );
227+
228+ for (Object o : a ) {
229+ JSONObject jo = (JSONObject ) o ;
230+ String name = jo .getString ("name" );
231+
232+ ParameterDefinition d = prop .getParameterDefinition (name );
233+ if (d == null ) throw new IllegalArgumentException ("No such parameter definition: " + name );
234+ ParameterValue parameterValue = d .createValue (req , jo );
235+ if (parameterValue != null ) {
236+ values .add (parameterValue );
237+ } else {
238+ throw new IllegalArgumentException ("Cannot retrieve the parameter value: " + name );
239+ }
240+ }
241+ }
242+ }
243+ }
244+
245+ ScheduledRun sr = new ScheduledRun (
246+ id , target .getFullName (), startDateTime , values , triggerOnMissed , new ScheduledBuildCause ());
247+
248+ ScheduleBuildGlobalConfiguration .get ().getScheduledRuns ().add (sr );
249+ ScheduleBuildGlobalConfiguration .get ().save ();
250+
251+ rsp .sendRedirect2 (".." );
252+ } catch (Exception e ) {
253+ throw new RuntimeException (e );
254+ }
255+ }
256+
257+ public boolean hasPlannedBuilds () {
258+ for (ScheduledRun sr : ScheduleBuildGlobalConfiguration .get ().getScheduledRuns ()) {
259+ if (sr .getJob ().equals (target .getFullName ())) {
260+ return true ;
261+ }
262+ }
263+ return false ;
264+ }
265+
266+ public List <ScheduledRun > getPlannedBuilds () {
267+ return ScheduleBuildGlobalConfiguration .get ().getScheduledRuns ().stream ()
268+ .filter (r -> r .getJob ().equals (target .getFullName ()))
269+ .toList ();
270+ }
271+
272+ public boolean isParameterized () {
273+ if (target instanceof ParameterizedJobMixIn .ParameterizedJob <?, ?> pj ) {
274+ return pj .isParameterized ();
275+ }
276+ return false ;
277+ }
278+
279+ public List <ParameterDefinition > getParameterDefinitions () {
280+ if (target instanceof ParameterizedJobMixIn .ParameterizedJob <?, ?> pj ) {
281+ if (pj .isParameterized ()) {
282+ return target .getProperty (ParametersDefinitionProperty .class ).getParameterDefinitions ();
283+ }
284+ }
285+ return List .of ();
286+ }
287+
133288 @ RequirePOST
134289 public HttpResponse doNext (@ QueryParameter String date , @ AncestorInPath Item item ) {
135290 if (item == null ) {
@@ -150,7 +305,7 @@ public HttpResponse doNext(@QueryParameter String date, @AncestorInPath Item ite
150305 quietperiod = ChronoUnit .SECONDS .between (now , ddate );
151306 LOGGER .log (Level .FINER , () -> "Quietperiod: " + quietperiod );
152307 if (quietperiod + ScheduleBuildAction .SECURITY_MARGIN < 0 ) { // 120 sec security margin
153- LOGGER .log (Level .INFO , () -> "Error security margin" + quietperiod );
308+ LOGGER .log (Level .INFO , () -> "Error security margin " + quietperiod );
154309 return HttpResponses .redirectTo ("error" );
155310 }
156311 return HttpResponses .forwardToView (this , "redirect" );
@@ -169,13 +324,6 @@ private LocalDateTime parseDateTime(String time) {
169324 throw exception ;
170325 }
171326
172- public boolean isJobParameterized () {
173- ParametersDefinitionProperty paramDefinitions = target .getProperty (ParametersDefinitionProperty .class );
174- return paramDefinitions != null
175- && paramDefinitions .getParameterDefinitions () != null
176- && paramDefinitions .getParameterDefinitions ().size () > 0 ;
177- }
178-
179327 @ Restricted (NoExternalUse .class )
180328 public String getDateTimeFormatting () {
181329 return DATE_TIME_PATTERN ;
0 commit comments