001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.petstore.console.pets;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022import java.util.concurrent.*;
023import java.util.function.*;
024
025import org.apache.juneau.commons.beanquery.*;
026import org.apache.juneau.http.*;
027import org.apache.juneau.http.response.*;
028import org.apache.juneau.marshall.collections.*;
029import org.apache.juneau.petstore.console.*;
030import org.apache.juneau.petstore.console.pages.*;
031import org.apache.juneau.petstore.service.*;
032import org.apache.juneau.rest.server.*;
033import org.apache.juneau.console.datatables.*;
034import org.apache.juneau.console.datatables.adapter.*;
035import org.apache.juneau.rest.server.view.*;
036import org.apache.juneau.rest.server.view.freemarker.*;
037import org.apache.juneau.console.views.*;
038
039/**
040 * Pending changes: the staged-edits table.  Persistent selection is limited to {@code PENDING} rows, an aggregate
041 * <b>Discard</b> returns a {@link BulkResult}, a perRow <b>Apply</b> writes each change to its pet, and a perRow
042 * <b>Retry</b> re-applies a failed change.  Also the data source of the header pending-changes badge.
043 *
044 * <h5 class='section'>Example:</h5>
045 * <p class='bcode'>
046 *    selection: { rowIdField:'id', selectAll:true, scope:'persistent', labelField:'summary',
047 *                 selectableWhen:[{field:'state', op:'eq', value:'PENDING', reason:'Only pending changes can be discarded or applied.'}] },
048 *    bulk: { actions: [
049 *      { id:'discard', label:'Discard', bulkMode:'aggregate', endpoint:'/console/pets/changes/discard', method:'POST', tone:'danger' },
050 *      { id:'apply',   label:'Apply',   bulkMode:'perRow',    endpoint:'/console/pets/changes/{id}/apply', method:'POST' } ] }
051 * </p>
052 * <p class='bjava'>
053 *    <ja>@RestPost</ja>(path=<js>"/discard"</js>)
054 *    <jk>public</jk> BulkResult discard(RestRequest <jv>req</jv>, <ja>@Content</ja> BulkRequest <jv>body</jv>) {
055 *       <jk>var</jk> <jv>o</jv> = store().pendingChanges().discard(<jv>body</jv>.<jf>ids</jf>, ConsoleWrites.<jsm>actor</jsm>(<jv>req</jv>));
056 *       <jk>var</jk> <jv>b</jv> = BulkResult.<jsm>create</jsm>().succeeded(<jv>o</jv>.succeeded().toArray(String[]::<jk>new</jk>)).notFound(...);
057 *       <jv>o</jv>.failed().forEach(<jv>f</jv> -&gt; <jv>b</jv>.failed(<jk>new</jk> BulkResult.Failure(<jv>f</jv>.id(), <jv>f</jv>.message())));
058 *       <jk>return</jk> <jv>b</jv>.build();
059 *    }
060 * </p>
061 */
062@Rest(path="/changes", title="Pending changes")
063@SuppressWarnings({
064   "java:S110" // Inheritance depth comes from the BasicRestServlet hierarchy, not this page.
065})
066public class ChangesRest extends PetstoreConsolePage {
067
068   private static final long serialVersionUID = 1L;
069
070   /** Replayed discard results by idempotency key (demo-sized; cleared when it grows past the bound). */
071   private final transient Map<String,BulkResult> replays = new ConcurrentHashMap<>();
072
073   /**
074    * The Pending changes page.
075    *
076    * @param req The request; {@code ?builder} renders the builder twin.
077    * @return The page.
078    */
079   @RestGet(path="/")
080   public View page(RestRequest req) {
081      return builder(req) ? twin(req, ChangesPageBuilders.changes()) : FreemarkerView.of("changes.ftlh");
082   }
083
084   /**
085    * Server-mode DataTables query over staged changes.
086    *
087    * @param req The DataTables request.
088    * @return One page of changes, or a 200 {@code error} envelope for a malformed search expression.
089    */
090   @RestPost(path="/query")
091   public DataTablesResults<PendingChange> query(@Content DataTablesRequest req) {
092      try (var s = store().queryPendingChanges()) {
093         return DataTablesQuery.run(req, s);
094      } catch (BeanQuerySyntaxException e) {
095         return DataTablesResults.error(req.getDraw(), e.getMessage());
096      }
097   }
098
099   /**
100    * The header badge source: pending changes, flagged {@code mine} for the caller.
101    *
102    * @param req The request.
103    * @return {@code {total, mine, items:[{id,label,href,mine,kind}]}}.
104    */
105   @RestGet(path="/pending")
106   public JsonMap pending(RestRequest req) {
107      var me = ConsoleWrites.author(ConsoleWrites.actor(req));
108      var items = new JsonList();
109      var mine = 0;
110      for (var c : store().pendingChanges().pending()) {
111         var isMine = eq(c.getAuthor(), me);
112         if (isMine)
113            mine++;
114         items.add(JsonMap.of("id", String.valueOf(c.getId()), "label", c.getSummary(), "href", f("/console/pets/%s/summary", c.getPetId()), "mine", isMine, "kind", "Pet"));
115      }
116      return JsonMap.of("total", items.size(), "mine", mine, "items", items);
117   }
118
119   /**
120    * Applies one change (the bulk perRow action).
121    *
122    * @param req The request.
123    * @param id The change id.
124    * @return {@code success} with the change when applied, {@code failure} with the reason when the write failed.
125    * @throws NotFound If the change does not exist.
126    * @throws Conflict If it is not pending.
127    */
128   @RestPost(path="/{id}/apply")
129   public ActionResult apply(RestRequest req, @Path("id") long id) {
130      var actor = ConsoleWrites.actor(req);
131      return outcome(call(() -> store().pendingChanges().apply(id, actor)));
132   }
133
134   /**
135    * Retries a failed change.
136    *
137    * @param req The request.
138    * @param id The change id.
139    * @return As {@link #apply(RestRequest, long)}.
140    * @throws NotFound If the change does not exist.
141    * @throws Conflict If it has not failed.
142    */
143   @RestPost(path="/{id}/retry")
144   public ActionResult retry(RestRequest req, @Path("id") long id) {
145      var actor = ConsoleWrites.actor(req);
146      return outcome(call(() -> store().pendingChanges().retry(id, actor)));
147   }
148
149   /**
150    * Discards changes in one aggregate call.  The same key gets the same answer.
151    *
152    * @param req The request.
153    * @param body {@code {ids, idempotencyKey}}.
154    * @param fail {@code discard} to simulate a transport failure (demo toggle).
155    * @return The per-id outcome.
156    * @throws BadRequest If the key or the ids are missing.
157    * @throws ServiceUnavailable On {@code ?fail=discard}.
158    */
159   @RestPost(path="/discard")
160   public BulkResult discard(RestRequest req, @Content BulkRequest body, @Query("fail") String fail) {
161      var actor = ConsoleWrites.actor(req);
162      if ("discard".equals(fail))
163         throw new ServiceUnavailable("Discard is unavailable (demo toggle '%s')", fail);
164      if (body.ids == null)
165         throw new BadRequest("Missing 'ids'");
166      var key = body.idempotencyKey;
167      if (ib(key))
168         throw new BadRequest("Missing 'idempotencyKey'");
169      if (replays.size() > 1_000)
170         replays.clear();
171      return replays.computeIfAbsent(key, k -> {
172         var o = store().pendingChanges().discard(body.ids, actor);
173         var b = BulkResult.create().succeeded(o.succeeded().toArray(String[]::new)).notFound(o.notFound().toArray(String[]::new));
174         o.failed().forEach(x -> b.failed(new BulkResult.Failure(x.id(), x.message())));
175         return b.build();
176      });
177   }
178
179   private static ActionResult outcome(PendingChange c) {
180      return c.getState() == PendingChange.State.APPLIED
181         ? ActionResult.success(c).message(f("Applied '%s'", c.getSummary()))
182         : ActionResult.failure().row(c).message(c.getMessage());
183   }
184
185   private static PendingChange call(Supplier<PendingChange> s) {
186      try {
187         return s.get();
188      } catch (PetstoreNotFoundException e) {
189         throw new NotFound(e.getMessage());
190      } catch (PetstoreConflictException e) {
191         throw new Conflict(e.getMessage());
192      }
193   }
194}