forked from community/device-mgt-core
API endpoint to get all devices which are enrolled between two dates ## Purpose > Building a new feature which lets users to generate reports according to various parameters. ## Goals > Get all the devices which enrolled between two dates. Use as a one feature of report generating feature. ## Approach * Created a new service for reports. * Added a new method to DeviceDAO to get reports between two dates. * Called the DeviceDAO in Reports service and called the above method ## Documentation **New API endpoints** : * Get all devices between two dates : /reports/devices?from=[date]&to=[date] * Filter by device status : /reports/devices?status=[status]&from=[date]&to=[date] * Filter by device ownership : /reports/devices?ownership=[ownership]&from=[date]&to=[date] Date format : YYYY-MM-DD See merge request entgra/carbon-device-mgt!237feature/appm-store/pbac
commit
52de9b85e9
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Entgra (pvt) Ltd. licenses this file to you 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.wso2.carbon.device.mgt.jaxrs.service.api;
|
||||||
|
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import io.swagger.annotations.ApiParam;
|
||||||
|
import io.swagger.annotations.ApiResponse;
|
||||||
|
import io.swagger.annotations.ApiResponses;
|
||||||
|
import io.swagger.annotations.Extension;
|
||||||
|
import io.swagger.annotations.ExtensionProperty;
|
||||||
|
import io.swagger.annotations.Info;
|
||||||
|
import io.swagger.annotations.ResponseHeader;
|
||||||
|
import io.swagger.annotations.SwaggerDefinition;
|
||||||
|
import io.swagger.annotations.Tag;
|
||||||
|
import org.wso2.carbon.apimgt.annotations.api.Scope;
|
||||||
|
import org.wso2.carbon.apimgt.annotations.api.Scopes;
|
||||||
|
import org.wso2.carbon.device.mgt.common.exceptions.ReportManagementException;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.beans.DeviceList;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.beans.ErrorResponse;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.util.Constants;
|
||||||
|
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
@SwaggerDefinition(
|
||||||
|
info = @Info(
|
||||||
|
version = "1.0.0",
|
||||||
|
title = "",
|
||||||
|
extensions = {
|
||||||
|
@Extension(properties = {
|
||||||
|
@ExtensionProperty(name = "name", value = "DeviceReportnManagement"),
|
||||||
|
@ExtensionProperty(name = "context", value = "/api/device-mgt/v1.0/reports"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
tags = {
|
||||||
|
@Tag(name = "device_management", description = "")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@Scopes(
|
||||||
|
scopes = {
|
||||||
|
@Scope(
|
||||||
|
name = "Getting Details of Registered Devices",
|
||||||
|
description = "Getting Details of Registered Devices",
|
||||||
|
key = "perm:devices:view",
|
||||||
|
permissions = {"/device-mgt/devices/owning-device/view"}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Api(value = "Device Report Management", description = "Device report related operations can be found here.")
|
||||||
|
@Path("/reports")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public interface ReportManagementService {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/devices")
|
||||||
|
@ApiOperation(
|
||||||
|
produces = MediaType.APPLICATION_JSON,
|
||||||
|
httpMethod = "GET",
|
||||||
|
value = "Getting Details of Registered Devices",
|
||||||
|
notes = "Provides details of all the devices enrolled with WSO2 IoT Server.",
|
||||||
|
tags = "Device Management",
|
||||||
|
extensions = {
|
||||||
|
@Extension(properties = {
|
||||||
|
@ExtensionProperty(name = Constants.SCOPE, value = "perm:devices:view")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
code = 200,
|
||||||
|
message = "OK. \n Successfully fetched the list of devices.",
|
||||||
|
response = DeviceList.class,
|
||||||
|
responseHeaders = {
|
||||||
|
@ResponseHeader(
|
||||||
|
name = "Content-Type",
|
||||||
|
description = "The content type of the body"),
|
||||||
|
@ResponseHeader(
|
||||||
|
name = "ETag",
|
||||||
|
description = "Entity Tag of the response resource.\n" +
|
||||||
|
"Used by caches, or in conditional requests."),
|
||||||
|
@ResponseHeader(
|
||||||
|
name = "Last-Modified",
|
||||||
|
description = "Date and time the resource was last modified.\n" +
|
||||||
|
"Used by caches, or in conditional requests."),
|
||||||
|
}),
|
||||||
|
@ApiResponse(
|
||||||
|
code = 400,
|
||||||
|
message = "Bad Request. \n Invalid device status type received. \n" +
|
||||||
|
"Valid status types are NEW | CHECKED",
|
||||||
|
response = ErrorResponse.class),
|
||||||
|
@ApiResponse(
|
||||||
|
code = 404,
|
||||||
|
message = "Not Found. \n There are no devices.",
|
||||||
|
response = ErrorResponse.class),
|
||||||
|
@ApiResponse(
|
||||||
|
code = 500,
|
||||||
|
message = "Internal Server Error. " +
|
||||||
|
"\n Server error occurred while fetching the device list.",
|
||||||
|
response = ErrorResponse.class)
|
||||||
|
})
|
||||||
|
Response getDevicesByDuration(
|
||||||
|
@ApiParam(
|
||||||
|
name = "status",
|
||||||
|
value = "Provide the device status details, such as active or inactive.")
|
||||||
|
@QueryParam("status") String status,
|
||||||
|
@ApiParam(
|
||||||
|
name = "ownership",
|
||||||
|
allowableValues = "BYOD, COPE",
|
||||||
|
value = "Provide the ownership status of the device. The following values can be assigned:\n" +
|
||||||
|
"- BYOD: Bring Your Own Device\n" +
|
||||||
|
"- COPE: Corporate-Owned, Personally-Enabled")
|
||||||
|
@QueryParam("ownership") String ownership,
|
||||||
|
@ApiParam(
|
||||||
|
name = "fromDate",
|
||||||
|
value = "Start date of the duration",
|
||||||
|
required = true)
|
||||||
|
@QueryParam("from") String fromDate,
|
||||||
|
@ApiParam(
|
||||||
|
name = "toDate",
|
||||||
|
value = "end date of the duration",
|
||||||
|
required = true)
|
||||||
|
@QueryParam("to") String toDate,
|
||||||
|
@ApiParam(
|
||||||
|
name = "offset",
|
||||||
|
value = "The starting pagination index for the complete list of qualified items.",
|
||||||
|
defaultValue = "0")
|
||||||
|
@QueryParam("offset")
|
||||||
|
int offset,
|
||||||
|
@ApiParam(
|
||||||
|
name = "limit",
|
||||||
|
value = "Provide how many device details you require from the starting pagination index/offset.",
|
||||||
|
defaultValue = "5")
|
||||||
|
@QueryParam("limit")
|
||||||
|
int limit) throws ReportManagementException;
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Entgra (pvt) Ltd. licenses this file to you 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.wso2.carbon.device.mgt.jaxrs.service.impl;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.wso2.carbon.device.mgt.common.Device;
|
||||||
|
import org.wso2.carbon.device.mgt.common.PaginationRequest;
|
||||||
|
import org.wso2.carbon.device.mgt.common.PaginationResult;
|
||||||
|
import org.wso2.carbon.device.mgt.common.exceptions.ReportManagementException;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.beans.DeviceList;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.beans.ErrorResponse;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.service.api.ReportManagementService;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.service.impl.util.RequestValidationUtil;
|
||||||
|
import org.wso2.carbon.device.mgt.jaxrs.util.DeviceMgtAPIUtils;
|
||||||
|
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the service class for report generating operations
|
||||||
|
*/
|
||||||
|
@Path("/reports")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public class ReportManagementServiceImpl implements ReportManagementService {
|
||||||
|
|
||||||
|
private static final Log log = LogFactory.getLog(ReportManagementServiceImpl.class);
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/devices")
|
||||||
|
@Override
|
||||||
|
public Response getDevicesByDuration(
|
||||||
|
@QueryParam("status") String status,
|
||||||
|
@QueryParam("ownership") String ownership,
|
||||||
|
@QueryParam("from") String fromDate,
|
||||||
|
@QueryParam("to") String toDate,
|
||||||
|
@DefaultValue("0")
|
||||||
|
@QueryParam("offset") int offset,
|
||||||
|
@DefaultValue("5")
|
||||||
|
@QueryParam("limit") int limit) {
|
||||||
|
try {
|
||||||
|
RequestValidationUtil.validatePaginationParameters(offset, limit);
|
||||||
|
PaginationRequest request = new PaginationRequest(offset, limit);
|
||||||
|
PaginationResult result;
|
||||||
|
DeviceList devices = new DeviceList();
|
||||||
|
|
||||||
|
if (!StringUtils.isBlank(status)) {
|
||||||
|
request.setStatus(status);
|
||||||
|
}
|
||||||
|
if (!StringUtils.isBlank(ownership)) {
|
||||||
|
request.setOwnership(ownership);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = DeviceMgtAPIUtils.getReportManagementService()
|
||||||
|
.getDevicesByDuration(request, fromDate, toDate);
|
||||||
|
if (result.getData().isEmpty()) {
|
||||||
|
String msg = "No devices have enrolled between " + fromDate + " to " + toDate +
|
||||||
|
" or doesn't match with" +
|
||||||
|
" given parameters";
|
||||||
|
log.error(msg);
|
||||||
|
return Response.status(Response.Status.OK).entity(msg).build();
|
||||||
|
} else {
|
||||||
|
devices.setList((List<Device>) result.getData());
|
||||||
|
devices.setCount(result.getRecordsTotal());
|
||||||
|
return Response.status(Response.Status.OK).entity(devices).build();
|
||||||
|
}
|
||||||
|
} catch (ReportManagementException e) {
|
||||||
|
String msg = "Error occurred while retrieving device list";
|
||||||
|
log.error(msg, e);
|
||||||
|
return Response.serverError().entity(
|
||||||
|
new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Entgra (pvt) Ltd. licenses this file to you 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.wso2.carbon.device.mgt.common.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used for exception handling in report generating operations
|
||||||
|
*/
|
||||||
|
public class ReportManagementException extends Exception {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -409298183404045217L;
|
||||||
|
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorMessage(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportManagementException(String msg, Exception nestedEx) {
|
||||||
|
super(msg, nestedEx);
|
||||||
|
setErrorMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportManagementException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
setErrorMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportManagementException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
setErrorMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportManagementException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportManagementException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Entgra (pvt) Ltd. licenses this file to you 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.wso2.carbon.device.mgt.common.report.mgt;
|
||||||
|
|
||||||
|
import org.wso2.carbon.device.mgt.common.PaginationRequest;
|
||||||
|
import org.wso2.carbon.device.mgt.common.PaginationResult;
|
||||||
|
import org.wso2.carbon.device.mgt.common.exceptions.ReportManagementException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the service class for reports which connects with DAO layer
|
||||||
|
*/
|
||||||
|
public interface ReportManagementService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used to call the getDevicesByDuration method from DeviceDAO
|
||||||
|
*
|
||||||
|
* @param request Pagination Request to get a paginated result
|
||||||
|
* @param fromDate Start date to filter devices(YYYY-MM-DD)
|
||||||
|
* @param toDate End date to filter devices(YYYY-MM-DD)
|
||||||
|
* @return {@link PaginationResult}
|
||||||
|
* @throws {@Link DeviceManagementException} When error occurred while validating device list page size
|
||||||
|
* @throws {@Link ReportManagementException} When failed to retrieve devices.
|
||||||
|
*/
|
||||||
|
PaginationResult getDevicesByDuration(PaginationRequest request, String fromDate, String toDate)
|
||||||
|
throws ReportManagementException;
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Entgra (pvt) Ltd. licenses this file to you 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.wso2.carbon.device.mgt.core.report.mgt;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.wso2.carbon.device.mgt.common.Device;
|
||||||
|
import org.wso2.carbon.device.mgt.common.exceptions.DeviceManagementException;
|
||||||
|
import org.wso2.carbon.device.mgt.common.PaginationRequest;
|
||||||
|
import org.wso2.carbon.device.mgt.common.PaginationResult;
|
||||||
|
import org.wso2.carbon.device.mgt.common.exceptions.ReportManagementException;
|
||||||
|
import org.wso2.carbon.device.mgt.common.report.mgt.ReportManagementService;
|
||||||
|
import org.wso2.carbon.device.mgt.core.dao.DeviceDAO;
|
||||||
|
import org.wso2.carbon.device.mgt.core.dao.DeviceManagementDAOException;
|
||||||
|
import org.wso2.carbon.device.mgt.core.dao.DeviceManagementDAOFactory;
|
||||||
|
import org.wso2.carbon.device.mgt.core.dao.util.DeviceManagementDAOUtil;
|
||||||
|
import org.wso2.carbon.device.mgt.core.util.DeviceManagerUtil;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the service class for reports which calls dao classes and its method which are used for
|
||||||
|
* report generation tasks.
|
||||||
|
*/
|
||||||
|
public class ReportManagementServiceImpl implements ReportManagementService {
|
||||||
|
|
||||||
|
private static final Log log = LogFactory.getLog(ReportManagementServiceImpl.class);
|
||||||
|
|
||||||
|
private DeviceDAO deviceDAO;
|
||||||
|
|
||||||
|
public ReportManagementServiceImpl() {
|
||||||
|
this.deviceDAO = DeviceManagementDAOFactory.getDeviceDAO();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PaginationResult getDevicesByDuration(PaginationRequest request, String fromDate,
|
||||||
|
String toDate)
|
||||||
|
throws ReportManagementException {
|
||||||
|
PaginationResult paginationResult = new PaginationResult();
|
||||||
|
try {
|
||||||
|
request = DeviceManagerUtil.validateDeviceListPageSize(request);
|
||||||
|
} catch (DeviceManagementException e) {
|
||||||
|
String msg = "Error occurred while validating device list page size";
|
||||||
|
log.error(msg, e);
|
||||||
|
throw new ReportManagementException(msg, e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DeviceManagementDAOFactory.openConnection();
|
||||||
|
List<Device> devices = deviceDAO.getDevicesByDuration(
|
||||||
|
request,
|
||||||
|
DeviceManagementDAOUtil.getTenantId(),
|
||||||
|
fromDate,
|
||||||
|
toDate
|
||||||
|
);
|
||||||
|
paginationResult.setData(devices);
|
||||||
|
//TODO: Should change the following code to a seperate count method from deviceDAO to get the count
|
||||||
|
paginationResult.setRecordsTotal(devices.size());
|
||||||
|
return paginationResult;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
String msg = "Error occurred while opening a connection " +
|
||||||
|
"to the data source";
|
||||||
|
log.error(msg, e);
|
||||||
|
throw new ReportManagementException(msg, e);
|
||||||
|
} catch (DeviceManagementDAOException e) {
|
||||||
|
String msg = "Error occurred while retrieving Tenant ID";
|
||||||
|
log.error(msg, e);
|
||||||
|
throw new ReportManagementException(msg, e);
|
||||||
|
} finally {
|
||||||
|
DeviceManagementDAOFactory.closeConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue