001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rest;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertNotNull;
022import static org.junit.Assert.assertNull;
023import static org.junit.Assert.assertTrue;
024
025import com.fasterxml.jackson.databind.ObjectMapper;
026import java.io.ByteArrayInputStream;
027import java.io.IOException;
028import java.io.StringWriter;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034import javax.xml.bind.JAXBContext;
035import javax.xml.bind.JAXBException;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.hbase.HBaseClassTestRule;
038import org.apache.hadoop.hbase.HBaseTestingUtil;
039import org.apache.hadoop.hbase.NamespaceDescriptor;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.client.Admin;
042import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
043import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
044import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
045import org.apache.hadoop.hbase.rest.client.Client;
046import org.apache.hadoop.hbase.rest.client.Cluster;
047import org.apache.hadoop.hbase.rest.client.Response;
048import org.apache.hadoop.hbase.rest.model.NamespacesInstanceModel;
049import org.apache.hadoop.hbase.rest.model.TableListModel;
050import org.apache.hadoop.hbase.rest.model.TableModel;
051import org.apache.hadoop.hbase.rest.model.TestNamespacesInstanceModel;
052import org.apache.hadoop.hbase.testclassification.MediumTests;
053import org.apache.hadoop.hbase.testclassification.RestTests;
054import org.apache.hadoop.hbase.util.Bytes;
055import org.apache.http.Header;
056import org.junit.AfterClass;
057import org.junit.BeforeClass;
058import org.junit.ClassRule;
059import org.junit.Ignore;
060import org.junit.Test;
061import org.junit.experimental.categories.Category;
062
063import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
064import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
065
066@Category({ RestTests.class, MediumTests.class })
067public class TestNamespacesInstanceResource {
068  @ClassRule
069  public static final HBaseClassTestRule CLASS_RULE =
070    HBaseClassTestRule.forClass(TestNamespacesInstanceResource.class);
071
072  private static String NAMESPACE1 = "TestNamespacesInstanceResource1";
073  private static Map<String, String> NAMESPACE1_PROPS = new HashMap<>();
074  private static String NAMESPACE2 = "TestNamespacesInstanceResource2";
075  private static Map<String, String> NAMESPACE2_PROPS = new HashMap<>();
076  private static String NAMESPACE3 = "TestNamespacesInstanceResource3";
077  private static Map<String, String> NAMESPACE3_PROPS = new HashMap<>();
078  private static String NAMESPACE4 = "TestNamespacesInstanceResource4";
079  private static Map<String, String> NAMESPACE4_PROPS = new HashMap<>();
080
081  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
082  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
083  private static Client client;
084  private static JAXBContext context;
085  private static Configuration conf;
086  private static TestNamespacesInstanceModel testNamespacesInstanceModel;
087  protected static ObjectMapper jsonMapper;
088
089  @BeforeClass
090  public static void setUpBeforeClass() throws Exception {
091    conf = TEST_UTIL.getConfiguration();
092    TEST_UTIL.startMiniCluster();
093    REST_TEST_UTIL.startServletContainer(conf);
094    client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()));
095    testNamespacesInstanceModel = new TestNamespacesInstanceModel();
096    context = JAXBContext.newInstance(NamespacesInstanceModel.class, TableListModel.class);
097    jsonMapper = new JacksonJaxbJsonProvider().locateMapper(NamespacesInstanceModel.class,
098      MediaType.APPLICATION_JSON_TYPE);
099    NAMESPACE1_PROPS.put("key1", "value1");
100    NAMESPACE2_PROPS.put("key2a", "value2a");
101    NAMESPACE2_PROPS.put("key2b", "value2b");
102    NAMESPACE3_PROPS.put("key3", "value3");
103    NAMESPACE4_PROPS.put("key4a", "value4a");
104    NAMESPACE4_PROPS.put("key4b", "value4b");
105  }
106
107  @AfterClass
108  public static void tearDownAfterClass() throws Exception {
109    REST_TEST_UTIL.shutdownServletContainer();
110    TEST_UTIL.shutdownMiniCluster();
111  }
112
113  private static byte[] toXML(NamespacesInstanceModel model) throws JAXBException {
114    StringWriter writer = new StringWriter();
115    context.createMarshaller().marshal(model, writer);
116    return Bytes.toBytes(writer.toString());
117  }
118
119  @SuppressWarnings("unchecked")
120  private static <T> T fromXML(byte[] content) throws JAXBException {
121    return (T) context.createUnmarshaller().unmarshal(new ByteArrayInputStream(content));
122  }
123
124  private NamespaceDescriptor findNamespace(Admin admin, String namespaceName) throws IOException {
125    NamespaceDescriptor[] nd = admin.listNamespaceDescriptors();
126    for (NamespaceDescriptor namespaceDescriptor : nd) {
127      if (namespaceDescriptor.getName().equals(namespaceName)) {
128        return namespaceDescriptor;
129      }
130    }
131    return null;
132  }
133
134  private void checkNamespaceProperties(NamespaceDescriptor nd, Map<String, String> testProps) {
135    checkNamespaceProperties(nd.getConfiguration(), testProps);
136  }
137
138  private void checkNamespaceProperties(Map<String, String> namespaceProps,
139    Map<String, String> testProps) {
140    assertTrue(namespaceProps.size() == testProps.size());
141    for (String key : testProps.keySet()) {
142      assertEquals(testProps.get(key), namespaceProps.get(key));
143    }
144  }
145
146  private void checkNamespaceTables(List<TableModel> namespaceTables, List<String> testTables) {
147    assertEquals(namespaceTables.size(), testTables.size());
148    for (TableModel namespaceTable : namespaceTables) {
149      String tableName = namespaceTable.getName();
150      assertTrue(testTables.contains(tableName));
151    }
152  }
153
154  @Test
155  public void testCannotDeleteDefaultAndHbaseNamespaces() throws IOException {
156    String defaultPath = "/namespaces/default";
157    String hbasePath = "/namespaces/hbase";
158    Response response;
159
160    // Check that doesn't exist via non-REST call.
161    Admin admin = TEST_UTIL.getAdmin();
162    assertNotNull(findNamespace(admin, "default"));
163    assertNotNull(findNamespace(admin, "hbase"));
164
165    // Try (but fail) to delete namespaces via REST.
166    response = client.delete(defaultPath);
167    assertEquals(503, response.getCode());
168    response = client.delete(hbasePath);
169    assertEquals(503, response.getCode());
170
171    assertNotNull(findNamespace(admin, "default"));
172    assertNotNull(findNamespace(admin, "hbase"));
173  }
174
175  @Test
176  public void testGetNamespaceTablesAndCannotDeleteNamespace() throws IOException, JAXBException {
177    Admin admin = TEST_UTIL.getAdmin();
178    String nsName = "TestNamespacesInstanceResource5";
179    Response response;
180
181    // Create namespace via admin.
182    NamespaceDescriptor.Builder nsBuilder = NamespaceDescriptor.create(nsName);
183    NamespaceDescriptor nsd = nsBuilder.build();
184    nsd.setConfiguration("key1", "value1");
185    admin.createNamespace(nsd);
186
187    // Create two tables via admin.
188    TableName tn1 = TableName.valueOf(nsName + ":table1");
189    TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tn1);
190    ColumnFamilyDescriptor columnFamilyDescriptor =
191      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf1")).build();
192    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
193    admin.createTable(tableDescriptorBuilder.build());
194    TableName tn2 = TableName.valueOf(nsName + ":table2");
195    tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tn2);
196    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
197    admin.createTable(tableDescriptorBuilder.build());
198
199    Map<String, String> nsProperties = new HashMap<>();
200    nsProperties.put("key1", "value1");
201    List<String> nsTables = Arrays.asList("table1", "table2");
202
203    // Check get namespace properties as XML, JSON and Protobuf.
204    String namespacePath = "/namespaces/" + nsName;
205    response = client.get(namespacePath);
206    assertEquals(200, response.getCode());
207
208    response = client.get(namespacePath, Constants.MIMETYPE_XML);
209    assertEquals(200, response.getCode());
210    NamespacesInstanceModel model = fromXML(response.getBody());
211    checkNamespaceProperties(model.getProperties(), nsProperties);
212
213    response = client.get(namespacePath, Constants.MIMETYPE_JSON);
214    assertEquals(200, response.getCode());
215    model = jsonMapper.readValue(response.getBody(), NamespacesInstanceModel.class);
216    checkNamespaceProperties(model.getProperties(), nsProperties);
217
218    response = client.get(namespacePath, Constants.MIMETYPE_PROTOBUF);
219    assertEquals(200, response.getCode());
220    model.getObjectFromMessage(response.getBody());
221    checkNamespaceProperties(model.getProperties(), nsProperties);
222
223    // Check get namespace tables as XML, JSON and Protobuf.
224    namespacePath = "/namespaces/" + nsName + "/tables";
225    response = client.get(namespacePath);
226    assertEquals(200, response.getCode());
227
228    response = client.get(namespacePath, Constants.MIMETYPE_XML);
229    assertEquals(200, response.getCode());
230    TableListModel tablemodel = fromXML(response.getBody());
231    checkNamespaceTables(tablemodel.getTables(), nsTables);
232
233    response = client.get(namespacePath, Constants.MIMETYPE_JSON);
234    assertEquals(200, response.getCode());
235    tablemodel = jsonMapper.readValue(response.getBody(), TableListModel.class);
236    checkNamespaceTables(tablemodel.getTables(), nsTables);
237
238    response = client.get(namespacePath, Constants.MIMETYPE_PROTOBUF);
239    assertEquals(200, response.getCode());
240    tablemodel.setTables(new ArrayList<>());
241    tablemodel.getObjectFromMessage(response.getBody());
242    checkNamespaceTables(tablemodel.getTables(), nsTables);
243
244    // Check cannot delete namespace via REST because it contains tables.
245    response = client.delete(namespacePath);
246    namespacePath = "/namespaces/" + nsName;
247    assertEquals(503, response.getCode());
248  }
249
250  @Ignore("HBASE-19210")
251  @Test
252  public void testInvalidNamespacePostsAndPuts() throws IOException, JAXBException {
253    String namespacePath1 = "/namespaces/" + NAMESPACE1;
254    String namespacePath2 = "/namespaces/" + NAMESPACE2;
255    String namespacePath3 = "/namespaces/" + NAMESPACE3;
256    NamespacesInstanceModel model1;
257    NamespacesInstanceModel model2;
258    NamespacesInstanceModel model3;
259    Response response;
260
261    // Check that namespaces don't exist via non-REST call.
262    Admin admin = TEST_UTIL.getAdmin();
263    assertNull(findNamespace(admin, NAMESPACE1));
264    assertNull(findNamespace(admin, NAMESPACE2));
265    assertNull(findNamespace(admin, NAMESPACE3));
266
267    model1 = testNamespacesInstanceModel.buildTestModel(NAMESPACE1, NAMESPACE1_PROPS);
268    testNamespacesInstanceModel.checkModel(model1, NAMESPACE1, NAMESPACE1_PROPS);
269    model2 = testNamespacesInstanceModel.buildTestModel(NAMESPACE2, NAMESPACE2_PROPS);
270    testNamespacesInstanceModel.checkModel(model2, NAMESPACE2, NAMESPACE2_PROPS);
271    model3 = testNamespacesInstanceModel.buildTestModel(NAMESPACE3, NAMESPACE3_PROPS);
272    testNamespacesInstanceModel.checkModel(model3, NAMESPACE3, NAMESPACE3_PROPS);
273
274    // Try REST post and puts with invalid content.
275    response = client.post(namespacePath1, Constants.MIMETYPE_JSON, toXML(model1));
276    assertEquals(500, response.getCode());
277    String jsonString = jsonMapper.writeValueAsString(model2);
278    response = client.put(namespacePath2, Constants.MIMETYPE_XML, Bytes.toBytes(jsonString));
279    assertEquals(400, response.getCode());
280    response = client.post(namespacePath3, Constants.MIMETYPE_PROTOBUF, toXML(model3));
281    assertEquals(500, response.getCode());
282
283    NamespaceDescriptor nd1 = findNamespace(admin, NAMESPACE1);
284    NamespaceDescriptor nd2 = findNamespace(admin, NAMESPACE2);
285    NamespaceDescriptor nd3 = findNamespace(admin, NAMESPACE3);
286    assertNull(nd1);
287    assertNull(nd2);
288    assertNull(nd3);
289  }
290
291  @Test
292  public void testNamespaceCreateAndDeleteXMLAndJSON() throws IOException, JAXBException {
293    String namespacePath1 = "/namespaces/" + NAMESPACE1;
294    String namespacePath2 = "/namespaces/" + NAMESPACE2;
295    NamespacesInstanceModel model1;
296    NamespacesInstanceModel model2;
297    Response response;
298
299    // Check that namespaces don't exist via non-REST call.
300    Admin admin = TEST_UTIL.getAdmin();
301    assertNull(findNamespace(admin, NAMESPACE1));
302    assertNull(findNamespace(admin, NAMESPACE2));
303
304    model1 = testNamespacesInstanceModel.buildTestModel(NAMESPACE1, NAMESPACE1_PROPS);
305    testNamespacesInstanceModel.checkModel(model1, NAMESPACE1, NAMESPACE1_PROPS);
306    model2 = testNamespacesInstanceModel.buildTestModel(NAMESPACE2, NAMESPACE2_PROPS);
307    testNamespacesInstanceModel.checkModel(model2, NAMESPACE2, NAMESPACE2_PROPS);
308
309    // Test cannot PUT (alter) non-existent namespace.
310    response = client.put(namespacePath1, Constants.MIMETYPE_XML, toXML(model1));
311    assertEquals(403, response.getCode());
312    String jsonString = jsonMapper.writeValueAsString(model2);
313    response = client.put(namespacePath2, Constants.MIMETYPE_JSON, Bytes.toBytes(jsonString));
314    assertEquals(403, response.getCode());
315
316    // Test cannot create tables when in read only mode.
317    conf.set("hbase.rest.readonly", "true");
318    response = client.post(namespacePath1, Constants.MIMETYPE_XML, toXML(model1));
319    assertEquals(403, response.getCode());
320    jsonString = jsonMapper.writeValueAsString(model2);
321    response = client.post(namespacePath2, Constants.MIMETYPE_JSON, Bytes.toBytes(jsonString));
322    assertEquals(403, response.getCode());
323    NamespaceDescriptor nd1 = findNamespace(admin, NAMESPACE1);
324    NamespaceDescriptor nd2 = findNamespace(admin, NAMESPACE2);
325    assertNull(nd1);
326    assertNull(nd2);
327    conf.set("hbase.rest.readonly", "false");
328
329    // Create namespace via XML and JSON.
330    response = client.post(namespacePath1, Constants.MIMETYPE_XML, toXML(model1));
331    assertEquals(201, response.getCode());
332    jsonString = jsonMapper.writeValueAsString(model2);
333    response = client.post(namespacePath2, Constants.MIMETYPE_JSON, Bytes.toBytes(jsonString));
334    assertEquals(201, response.getCode());
335    // check passing null content-type with a payload returns 415
336    Header[] nullHeaders = null;
337    response = client.post(namespacePath1, nullHeaders, toXML(model1));
338    assertEquals(415, response.getCode());
339    response = client.post(namespacePath1, nullHeaders, Bytes.toBytes(jsonString));
340    assertEquals(415, response.getCode());
341
342    // Check that created namespaces correctly.
343    nd1 = findNamespace(admin, NAMESPACE1);
344    nd2 = findNamespace(admin, NAMESPACE2);
345    assertNotNull(nd1);
346    assertNotNull(nd2);
347    checkNamespaceProperties(nd1, NAMESPACE1_PROPS);
348    checkNamespaceProperties(nd1, NAMESPACE1_PROPS);
349
350    // Test cannot delete tables when in read only mode.
351    conf.set("hbase.rest.readonly", "true");
352    response = client.delete(namespacePath1);
353    assertEquals(403, response.getCode());
354    response = client.delete(namespacePath2);
355    assertEquals(403, response.getCode());
356    nd1 = findNamespace(admin, NAMESPACE1);
357    nd2 = findNamespace(admin, NAMESPACE2);
358    assertNotNull(nd1);
359    assertNotNull(nd2);
360    conf.set("hbase.rest.readonly", "false");
361
362    // Delete namespaces via XML and JSON.
363    response = client.delete(namespacePath1);
364    assertEquals(200, response.getCode());
365    response = client.delete(namespacePath2);
366    assertEquals(200, response.getCode());
367    nd1 = findNamespace(admin, NAMESPACE1);
368    nd2 = findNamespace(admin, NAMESPACE2);
369    assertNull(nd1);
370    assertNull(nd2);
371  }
372
373  @Test
374  public void testNamespaceCreateAndDeletePBAndNoBody() throws IOException {
375    String namespacePath3 = "/namespaces/" + NAMESPACE3;
376    String namespacePath4 = "/namespaces/" + NAMESPACE4;
377    NamespacesInstanceModel model3;
378    NamespacesInstanceModel model4;
379    Response response;
380
381    // Check that namespaces don't exist via non-REST call.
382    Admin admin = TEST_UTIL.getAdmin();
383    assertNull(findNamespace(admin, NAMESPACE3));
384    assertNull(findNamespace(admin, NAMESPACE4));
385
386    model3 = testNamespacesInstanceModel.buildTestModel(NAMESPACE3, NAMESPACE3_PROPS);
387    testNamespacesInstanceModel.checkModel(model3, NAMESPACE3, NAMESPACE3_PROPS);
388    model4 = testNamespacesInstanceModel.buildTestModel(NAMESPACE4, NAMESPACE4_PROPS);
389    testNamespacesInstanceModel.checkModel(model4, NAMESPACE4, NAMESPACE4_PROPS);
390
391    // Defines null headers for use in tests where no body content is provided, so that we set
392    // no content-type in the request
393    Header[] nullHeaders = null;
394
395    // Test cannot PUT (alter) non-existent namespace.
396    response = client.put(namespacePath3, nullHeaders, new byte[] {});
397    assertEquals(403, response.getCode());
398    response =
399      client.put(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
400    assertEquals(403, response.getCode());
401
402    // Test cannot create tables when in read only mode.
403    conf.set("hbase.rest.readonly", "true");
404    response = client.post(namespacePath3, nullHeaders, new byte[] {});
405    assertEquals(403, response.getCode());
406    response =
407      client.put(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
408    assertEquals(403, response.getCode());
409    NamespaceDescriptor nd3 = findNamespace(admin, NAMESPACE3);
410    NamespaceDescriptor nd4 = findNamespace(admin, NAMESPACE4);
411    assertNull(nd3);
412    assertNull(nd4);
413    conf.set("hbase.rest.readonly", "false");
414
415    // Create namespace with no body and binary content type.
416    response = client.post(namespacePath3, nullHeaders, new byte[] {});
417    assertEquals(201, response.getCode());
418    // Create namespace with protobuf content-type.
419    response =
420      client.post(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
421    assertEquals(201, response.getCode());
422    // check setting unsupported content-type returns 415
423    response = client.post(namespacePath3, Constants.MIMETYPE_BINARY, new byte[] {});
424    assertEquals(415, response.getCode());
425
426    // Check that created namespaces correctly.
427    nd3 = findNamespace(admin, NAMESPACE3);
428    nd4 = findNamespace(admin, NAMESPACE4);
429    assertNotNull(nd3);
430    assertNotNull(nd4);
431    checkNamespaceProperties(nd3, new HashMap<>());
432    checkNamespaceProperties(nd4, NAMESPACE4_PROPS);
433
434    // Check cannot post tables that already exist.
435    response = client.post(namespacePath3, nullHeaders, new byte[] {});
436    assertEquals(403, response.getCode());
437    response =
438      client.post(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
439    assertEquals(403, response.getCode());
440
441    // Check cannot post tables when in read only mode.
442    conf.set("hbase.rest.readonly", "true");
443    response = client.delete(namespacePath3);
444    assertEquals(403, response.getCode());
445    response = client.delete(namespacePath4);
446    assertEquals(403, response.getCode());
447    nd3 = findNamespace(admin, NAMESPACE3);
448    nd4 = findNamespace(admin, NAMESPACE4);
449    assertNotNull(nd3);
450    assertNotNull(nd4);
451    conf.set("hbase.rest.readonly", "false");
452
453    // Delete namespaces via XML and JSON.
454    response = client.delete(namespacePath3);
455    assertEquals(200, response.getCode());
456    response = client.delete(namespacePath4);
457    assertEquals(200, response.getCode());
458    nd3 = findNamespace(admin, NAMESPACE3);
459    nd4 = findNamespace(admin, NAMESPACE4);
460    assertNull(nd3);
461    assertNull(nd4);
462  }
463}